Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slider-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": minor
---

Add `Slider` component for picking a number, or a range between two numbers, by dragging a thumb along a track. Built on Base UI's Slider: pass an array to `value` or `defaultValue` for a range. Shows each thumb's value in a badge below the track and the minimum and maximum at either end, formats them with `format`, and supports `sm` and `base` sizes.
1 change: 1 addition & 0 deletions packages/kumo-docs-astro/src/components/SidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const componentItems: NavItem[] = [
{ label: "Sensitive Input", href: "/components/sensitive-input" },
{ label: "Sidebar", href: "/components/sidebar" },
{ label: "Skeleton Line", href: "/components/skeleton-line" },
{ label: "Slider", href: "/components/slider" },
{ label: "Switch", href: "/components/switch" },
{ label: "Table", href: "/components/table" },
{ label: "Table of Contents", href: "/components/table-of-contents" },
Expand Down
13 changes: 13 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/HomeGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
Select,
SensitiveInput,
SkeletonLine,
Slider,
Switch,
Table,
TableOfContents,
Expand Down Expand Up @@ -91,6 +92,7 @@ const componentRoutes: Record<string, string> = {
select: "/components/select",
"sensitive-input": "/components/sensitive-input",
"skeleton-line": "/components/skeleton-line",
slider: "/components/slider",
switch: "/components/switch",
table: "/components/table",
"table-of-contents": "/components/table-of-contents",
Expand Down Expand Up @@ -558,6 +560,17 @@ export function HomeGrid() {
<Meter value={75} label="My meter" customValue="100 / 5,000" />
),
},
{
name: "Slider",
id: "slider",
Component: (
<Slider
defaultValue={[25, 75]}
getAriaLabel={index => (index === 0 ? "Minimum" : "Maximum")}
className="w-48"
/>
),
},
{
name: "DatePicker",
id: "date-picker",
Expand Down
69 changes: 69 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/SliderDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useState } from "react";
import { Slider } from "@cloudflare/kumo";

export function SliderBasicDemo() {
return (
<Slider label="Volume" defaultValue={40} className="w-72" />
);
}

export function SliderRangeDemo() {
return (
<Slider
label="Price range"
defaultValue={[25, 75]}
getAriaLabel={index => (index === 0 ? "Minimum price" : "Maximum price")}
className="w-72"
/>
);
}

export function SliderStepDemo() {
return (
<Slider label="Match count" defaultValue={2} max={5} className="w-72" />
);
}

export function SliderFormatDemo() {
return (
<Slider
label="Sampling rate"
defaultValue={0.25}
min={0}
max={1}
step={0.05}
format={{ style: "percent" }}
className="w-72"
/>
);
}

export function SliderSizesDemo() {
return (
<div className="flex w-72 flex-col gap-6">
<Slider label="Small" size="sm" defaultValue={30} />
<Slider label="Base" defaultValue={30} />
</div>
);
}

export function SliderControlledDemo() {
const [value, setValue] = useState(50);

return (
<div className="flex w-72 flex-col gap-3">
<Slider
label="Brightness"
value={value}
onValueChange={next => setValue(next)}
/>
<p className="text-sm text-kumo-subtle">Brightness: {value}%</p>
</div>
);
}

export function SliderDisabledDemo() {
return (
<Slider label="Locked" defaultValue={70} disabled className="w-72" />
);
}
142 changes: 142 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/slider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
layout: ~/layouts/MdxDocLayout.astro
title: "Slider"
description: "An input for picking a number, or a range between two numbers, by dragging a thumb along a track."
sourceFile: "components/slider"
---

import ComponentExample from "~/components/docs/ComponentExample.astro";
import ComponentSection from "~/components/docs/ComponentSection.astro";
import PropsTable from "~/components/docs/PropsTable.astro";
import {
SliderBasicDemo,
SliderRangeDemo,
SliderStepDemo,
SliderFormatDemo,
SliderSizesDemo,
SliderControlledDemo,
SliderDisabledDemo,
} from "~/components/demos/SliderDemo";

{/* Hero Demo */}

<ComponentSection>
<ComponentExample demo="SliderRangeDemo">
<SliderRangeDemo client:visible />
</ComponentExample>
</ComponentSection>

{/* Installation */}

<ComponentSection>

## Installation

### Barrel

```tsx
import { Slider } from "@cloudflare/kumo";
```

### Granular

```tsx
import { Slider } from "@cloudflare/kumo/components/slider";
```

</ComponentSection>

{/* Usage */}

<ComponentSection>

## Usage

```tsx
import { Slider } from "@cloudflare/kumo";

export default function Example() {
return <Slider label="Volume" defaultValue={40} />;
}
```

`Slider` is built on [Base UI's Slider](https://base-ui.com/react/components/slider). Props such as `value`, `defaultValue`, `min`, `max`, `step`, `format`, and `onValueChange` are passed through to it.

</ComponentSection>

{/* Examples */}

<ComponentSection>

## Examples

### Basic Slider

<p>A single thumb, with the value in a badge below it and the minimum and maximum at either end.</p>
<ComponentExample demo="SliderBasicDemo">
<SliderBasicDemo client:visible />
</ComponentExample>

### Range

<p>Pass an array to `value` or `defaultValue` to render one thumb per value. Use `getAriaLabel` to give each thumb its own accessible name.</p>
<ComponentExample demo="SliderRangeDemo">
<SliderRangeDemo client:visible />
</ComponentExample>

### Custom Range and Step

<p>Set `min`, `max`, and `step` to constrain the values.</p>
<ComponentExample demo="SliderStepDemo">
<SliderStepDemo client:visible />
</ComponentExample>

### Formatted Values

<p>`format` takes `Intl.NumberFormat` options and applies to the badges, the range labels, and the value announced to assistive technology.</p>
<ComponentExample demo="SliderFormatDemo">
<SliderFormatDemo client:visible />
</ComponentExample>

### Sizes

<p>Use `size="sm"` for dense layouts.</p>
<ComponentExample demo="SliderSizesDemo">
<SliderSizesDemo client:visible />
</ComponentExample>

### Controlled

<p>Pass `value` and `onValueChange` to control the slider.</p>
<ComponentExample demo="SliderControlledDemo">
<SliderControlledDemo client:visible />
</ComponentExample>

### Disabled

<ComponentExample demo="SliderDisabledDemo">
<SliderDisabledDemo client:visible />
</ComponentExample>

</ComponentSection>

{/* Accessibility */}

<ComponentSection>

## Accessibility

- Each thumb is a native range input, so arrow keys, <kbd>Page Up</kbd>/<kbd>Page Down</kbd>, and <kbd>Home</kbd>/<kbd>End</kbd> work by default.
- `label` names the slider and its thumb. Without a visible label, pass `getAriaLabel`.
- The value badges and range labels are hidden from assistive technology, because each thumb already announces its formatted value.

</ComponentSection>

{/* API Reference */}

<ComponentSection>

## API Reference

<PropsTable component="Slider" />
</ComponentSection>
4 changes: 4 additions & 0 deletions packages/kumo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@
"types": "./dist/components/sidebar.d.ts",
"import": "./dist/components/sidebar.js"
},
"./components/slider": {
"types": "./dist/components/slider.d.ts",
"import": "./dist/components/slider.js"
},
"./components/surface": {
"types": "./dist/components/surface.d.ts",
"import": "./dist/components/surface.js"
Expand Down
1 change: 1 addition & 0 deletions packages/kumo/scripts/component-registry/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const CATEGORY_MAP: Record<string, string> = {
"input-group": "Input",
radio: "Input",
select: "Input",
slider: "Input",
switch: "Input",
// Layout
grid: "Layout",
Expand Down
38 changes: 38 additions & 0 deletions packages/kumo/scripts/component-registry/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,44 @@ export const ADDITIONAL_COMPONENT_PROPS: Record<
description: "Minimum value of the meter (default: 0)",
},
},
Slider: {
value: {
type: "number | readonly number[]",
description:
"Controlled value. Pass an array to render one thumb per value (a range).",
},
defaultValue: {
type: "number | readonly number[]",
description:
"Uncontrolled initial value. Pass an array to render one thumb per value (a range).",
},
onValueChange: {
type: "(value: number | readonly number[], eventDetails: SliderRoot.ChangeEventDetails) => void",
description:
"Called when the value changes while dragging or from the keyboard",
},
min: {
type: "number",
description: "Minimum value of the slider (default: 0)",
},
max: {
type: "number",
description: "Maximum value of the slider (default: 100)",
},
step: {
type: "number",
description: "Amount the value changes per step (default: 1)",
},
format: {
type: "Intl.NumberFormatOptions",
description:
"Formats the value badges, range labels, and the value announced to assistive technology",
},
disabled: {
type: "boolean",
description: "Whether the slider is disabled",
},
},
Tooltip: {
content: {
type: "ReactNode",
Expand Down
9 changes: 9 additions & 0 deletions packages/kumo/src/components/slider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
Slider,
sliderVariants,
KUMO_SLIDER_VARIANTS,
KUMO_SLIDER_DEFAULT_VARIANTS,
type SliderProps,
type KumoSliderSize,
type KumoSliderVariantsProps,
} from "./slider";
Loading
Loading