# Slider

Chooses a number, or a range between two numbers, by dragging a handle along a track.

- **Import:** `import { Slider } from "@booleanpress/ui/slider"`
- **Radix Slider:** <https://www.radix-ui.com/primitives/docs/components/slider>
- **APG Slider:** <https://www.w3.org/WAI/ARIA/apg/patterns/slider/>
- **Page:** <https://ui.booleanpress.com/components/slider> · @booleanpress/ui 0.2.0

## Usage

A slider suits a value where the rough position matters more than the exact figure, such as a rate or a threshold. When people need an exact number, add an [input](/components/input) beside it, as in *With input*.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Slider } from "@booleanpress/ui/slider"

export function SendingRate() {
  return (
    <>
      <Label id="rate">Sending rate</Label>
      <Slider defaultValue={[50]} aria-labelledby="rate" />
    </>
  )
}
```

The value is always an array: one number for one handle, two for a range. It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`; `onValueCommit` fires once when a drag or a key press ends. `min`, `max` (0 and 100 by default) and `step` bound it, and `minStepsBetweenThumbs` keeps a range's handles apart.

Name it with `aria-labelledby` (a visible label's id) or `aria-label`: the slider passes the name to its handles, the parts with the `slider` role. A range's handles add their own names, "Minimum" and "Maximum" (the provider strings `sliderMinimum` and `sliderMaximum`; three or more handles use `sliderValue`). `size` is `sm`, `default` or `lg`, and follows the provider's `controlSize`. Show the value next to it yourself, formatted with `Intl` and the provider's locale (`useUiLocale`), as in *Controlled*. With `name`, it submits its value in a form.

## Examples

### Basic

One handle, from 0 to 100.

```tsx
import { Slider } from "@booleanpress/ui/slider"

export default function SliderBasic() {
  return <Slider defaultValue={[50]} aria-label="Sending rate" className="w-64" />
}
```

### Step

`step={20}` moves the value in steps of 20.

```tsx
import { Slider } from "@booleanpress/ui/slider"

export default function SliderStep() {
  return <Slider defaultValue={[20]} step={20} aria-label="Retry delay" className="w-64" />
}
```

### Range

Two values make two handles, named Minimum and Maximum.

```tsx
import { Slider } from "@booleanpress/ui/slider"

export default function SliderRange() {
  return <Slider defaultValue={[20, 80]} aria-label="Quiet hours" className="w-64" />
}
```

### Vertical

`orientation="vertical"`, each named by the caption under it.

```tsx
import { Slider } from "@booleanpress/ui/slider"

const QUEUES = [
  { id: "queue-transactional", label: "Orders", value: 70 },
  { id: "queue-marketing", label: "News", value: 20 },
  { id: "queue-alerts", label: "Alerts", value: 40 },
]

export default function SliderVertical() {
  return (
    <div className="flex gap-10">
      {QUEUES.map((queue) => (
        <div key={queue.id} className="flex flex-col items-center gap-3">
          <Slider
            orientation="vertical"
            defaultValue={[queue.value]}
            aria-labelledby={queue.id}
            className="h-28"
          />
          <span id={queue.id} className="text-xs/normal text-muted-foreground">
            {queue.label}
          </span>
        </div>
      ))}
    </div>
  )
}
```

### Controlled

`value` and `onValueChange`, with the value shown beside the label in the reader's number format.

```tsx
import { useState } from "react"
import { useUiLocale } from "@booleanpress/ui/provider"
import { Label } from "@booleanpress/ui/label"
import { Slider } from "@booleanpress/ui/slider"

export default function SliderControlled() {
  const [limit, setLimit] = useState([2500])
  const { locale } = useUiLocale()
  const formatted = new Intl.NumberFormat(locale).format(limit[0])

  return (
    <div className="flex w-64 flex-col gap-3">
      <div className="flex items-center justify-between gap-2">
        <Label id="daily-limit">Daily sending limit</Label>
        <span className="text-sm/normal text-muted-foreground tabular-nums">{formatted} emails</span>
      </div>
      <Slider value={limit} onValueChange={setLimit} min={0} max={10000} step={500} aria-labelledby="daily-limit" />
    </div>
  )
}
```

### With input

A number field and the slider share one value, so either can change it.

```tsx
import { useState } from "react"
import { Input } from "@booleanpress/ui/input"
import { Label } from "@booleanpress/ui/label"
import { Slider } from "@booleanpress/ui/slider"

export default function SliderWithInput() {
  const [value, setValue] = useState(50)

  return (
    <div className="flex w-64 flex-col gap-3">
      <Label htmlFor="batch-size">Batch size</Label>
      <Input
        id="batch-size"
        type="number"
        min={0}
        max={100}
        value={value}
        onChange={(event) => setValue(Math.min(100, Math.max(0, Number(event.target.value))))}
      />
      <Slider value={[value]} onValueChange={([next]) => setValue(next)} aria-label="Batch size" />
    </div>
  )
}
```

### Sizes

`size="sm"`, the default and `size="lg"`: the handle and the track scale together.

```tsx
import { Slider } from "@booleanpress/ui/slider"

export default function SliderSizes() {
  return (
    <div className="flex w-64 flex-col gap-8">
      <Slider size="sm" defaultValue={[30]} aria-label="Small" />
      <Slider defaultValue={[50]} aria-label="Default" />
      <Slider size="lg" defaultValue={[70]} aria-label="Large" />
    </div>
  )
}
```

### Disabled

`disabled` dims the slider and removes its handles from the tab order.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Slider } from "@booleanpress/ui/slider"

export default function SliderDisabled() {
  return (
    <div className="flex w-64 flex-col gap-8">
      <div className="flex flex-col gap-3">
        <Label id="disabled-rate">Sending rate</Label>
        <Slider defaultValue={[50]} disabled aria-labelledby="disabled-rate" />
      </div>
      <div className="flex flex-col gap-3">
        <Label id="disabled-window">Quiet hours</Label>
        <Slider defaultValue={[20, 80]} disabled aria-labelledby="disabled-window" />
      </div>
    </div>
  )
}
```

## Accessibility

**Semantics.** Each handle is `role="slider"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and `aria-orientation`. The track and range are decoration. With `name`, each value is also a hidden input for forms.

**Labels.** Pass `aria-labelledby` or `aria-label` to the slider; it names every handle. A range's handles are named with the slider's name and "Minimum" or "Maximum" from the provider strings. `aria-describedby` reaches the handles too, for a hint or an error.

**Focus.** Each handle is a tab stop; a disabled slider has none. Focus is a 1px `--ring` outline 2px outside the handle.

**Known limits.**

- The value is announced as a bare number. Radix does not take a text form of the value (`aria-valuetext`), so show the unit next to the slider.
- The handles cannot be disabled one at a time; `disabled` covers the whole slider.
- A slider is hard to set to an exact value by pointer; pair it with an input when the exact number matters.

### Keyboard

| Key | Behaviour |
| --- | --- |
| → or ↑ | Increases the value by one step (→ decreases it in right-to-left pages). |
| ← or ↓ | Decreases the value by one step (← increases it in right-to-left pages). |
| Shift + → | With any arrow key, moves ten steps that way. |
| Page Up or Page Down | Increases or decreases the value by ten steps. |
| Home or End | Sets the minimum or the maximum. |
| Tab | Moves to the next handle, then out of the slider. |

## API

### Slider

Renders Radix Slider.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `defaultValue` | `number[]` |  | The values at the start, one per handle, when it controls itself. `[min]` by default. |
| `dir` | `"ltr" \| "rtl"` |  | The reading direction. The provider's `dir` by default. |
| `disabled` | `boolean` |  | Dims the slider and ignores the pointer and the keyboard. |
| `form` | `string` |  |  |
| `inverted` | `boolean` |  | Whether the value runs the other way along the track. |
| `max` | `number` | `100` | The highest value. 100 by default. |
| `min` | `number` | `0` | The lowest value. 0 by default. |
| `minStepsBetweenThumbs` | `number` |  | The fewest steps a range's handles keep between them. 0 by default. |
| `name` | `string` |  | The form field name; the value is submitted with the form. |
| `onValueChange` | `((value: number[]) => void)` |  | Called with the new values while the handle moves. |
| `onValueCommit` | `((value: number[]) => void)` |  | Called with the values once, when a drag or a key press ends. |
| `orientation` | `"horizontal" \| "vertical"` |  | `horizontal` (default) or `vertical`. |
| `size` | `"default" \| "sm" \| "lg"` |  | The track and handle: `sm` a 16px handle, `default` 20px, `lg` 24px. The provider's `controlSize` by default. |
| `step` | `number` |  | The amount each move changes the value by. 1 by default. |
| `value` | `number[]` |  | The values, one per handle, when you control them. Pair it with `onValueChange`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="slider"` (Slider), and `data-size`.

**Provider strings:** `sliderMinimum`, `sliderMaximum`, `sliderValue` (`BooleanUIProvider`'s `strings`).

## Theming

The track is `--border`, the filled range `--primary`. The handle is a `--border` ring round a knob in `--background` (`--card` on hover); focus is `--ring`. A disabled slider is drawn at 60% opacity.

| Token | Used for |
| --- | --- |
| `--background` | background |
| `--border` | background |
| `--card` | background |
| `--primary` | background |
| `--ring` | outline |
