# Slider

> Picks a number, or a range with two thumbs, by dragging along a track.

Source: /docs/components/slider

```tsx
import { Slider } from '@aksornproject/aksara-ui'
```

## Single value

A number in `value` or `defaultValue` gives one thumb. Pass exactly one of the two. `min`, `max` and `step` default to 0, 100 and 1. The slider fills its container, so set a width on the parent or with `className`.

```tsx
import { Slider } from '@aksornproject/aksara-ui'

export default function SliderBasic() {
  return (
    <div className="w-full max-w-spacing-320">
      <Slider label="Volume" defaultValue={50} />
    </div>
  )
}
```

## Range

A pair, `[start, end]`, gives two thumbs. There is no `mode` prop, because the shape of the value picks it. `onValueChange` fires on every drag move and key press, and `onValueCommitted` fires once when the drag ends. A range handler receives a `SliderRange`. `minStepsBetweenValues` keeps the thumbs apart.

```tsx
import { Slider, type SliderRange } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function SliderRangeDemo() {
  const [price, setPrice] = useState<SliderRange>([200, 600])

  return (
    <div className="w-full max-w-spacing-320">
      <Slider
        label="Price"
        value={price}
        onValueChange={setPrice}
        max={1000}
        step={50}
        formatValue={(value) => `฿${value}`}
        thumbLabels={['minimum', 'maximum']}
        valueDisplay="text"
      />
    </div>
  )
}
```

## Value display

`valueDisplay` picks where the value shows. `tooltip`, the default, shows a bubble above a thumb while it is hovered, dragged or keyboard focused. `text` shows the value beside the track and reserves the width of the value at `min` and `max`, so the track never resizes under the pointer. `none` hides it. `formatValue` writes the bubble, the text and what screen readers announce.

```tsx
import { Slider } from '@aksornproject/aksara-ui'

export default function SliderValueDisplay() {
  return (
    <div className="flex w-full max-w-spacing-320 flex-col gap-spacing-24">
      <Slider label="Tooltip" defaultValue={30} valueDisplay="tooltip" />
      <Slider
        label="Text"
        defaultValue={60}
        valueDisplay="text"
        formatValue={(value) => `${value}%`}
      />
      <Slider label="None" defaultValue={80} valueDisplay="none" />
    </div>
  )
}
```

## Marks

`marks` draws ticks on the track. A number draws a tick, and `{ value, label }` also draws a label under it. Marks outside `min` and `max` are dropped. Marks do not snap the value. `step` does.

```tsx
import { Slider } from '@aksornproject/aksara-ui'

const GRADES = [1, 2, 3, 4, 5, 6].map((value) => ({
  value,
  label: `P${value}`,
}))

export default function SliderMarks() {
  return (
    <div className="w-full max-w-spacing-320">
      <Slider
        label="Grade level"
        defaultValue={[2, 4]}
        min={1}
        max={6}
        marks={GRADES}
        formatValue={(value) => `P${value}`}
      />
    </div>
  )
}
```

## Vertical

`orientation="vertical"` gives a fixed 160px control. Mark labels sit to the right of their ticks.

```tsx
import { Slider } from '@aksornproject/aksara-ui'

export default function SliderVertical() {
  return (
    <Slider
      aria-label="Zoom"
      defaultValue={40}
      orientation="vertical"
      valueDisplay="text"
      formatValue={(value) => `${value}%`}
    />
  )
}
```

## Field

`required` draws a star after `label`, and `optional` draws "(Optional)" or a string you pass. Both are visual only, and a slider takes one or the other. `info` puts a help icon after the label that opens a tooltip. `description` sits under the label. `status` and `message` render the shared field message under the slider. While `disabled`, `disabledMessage` takes the place of `message` to say why.

```tsx
import { Slider } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function SliderField() {
  const [minutes, setMinutes] = useState(75)

  return (
    <div className="flex w-full max-w-spacing-320 flex-col gap-spacing-24">
      <Slider
        label="Quiz time"
        required
        info="Students see a countdown with this many minutes."
        description="How long students have to finish."
        value={minutes}
        onValueChange={setMinutes}
        min={10}
        max={90}
        step={5}
        formatValue={(value) => `${value} min`}
        status={minutes > 60 ? 'warning' : undefined}
        message={
          minutes > 60 ? 'Long quizzes tire younger students.' : undefined
        }
      />
      <Slider
        label="Passing score"
        optional
        defaultValue={50}
        disabled
        disabledMessage="Turn on grading to set a passing score."
      />
    </div>
  )
}
```

## Accessibility

- Each thumb is a native `<input type="range">`. Arrow keys step. Page Up, Page Down and Shift+Arrow step by `largeStep`. Home and End jump to the bounds.
- A slider needs exactly one name, a visible `label` or `aria-label` or `aria-labelledby`. TypeScript rejects none and both.
- A range thumb takes the slider name plus "start" or "end", so a screen reader hears "Price start". `thumbLabels` replaces the suffixes.
- Each thumb announces `formatValue` as its `aria-valuetext`. The bubble and the value text are hidden from screen readers, so nothing is read twice.
- `description`, `message` and `disabledMessage` are linked to every thumb with `aria-describedby`, after any ids you pass.
- `status="error"` cannot set `aria-invalid` on the thumb. The error message reaches screen readers through `aria-describedby`.
- In macOS Safari without Full Keyboard Access, Tab skips the slider, as it skips every native range input.

## API

### Slider

`V` is the value type, `number` or `SliderRange`. `name` submits each thumb's value through a hidden input. The rest of the native `div` props go to the root.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `SliderText` |  | The visible label above the slider. Clicking it focuses a single thumb. |
| `info` | `ReactNode` |  | Tooltip content behind a help icon after the label. |
| `infoLabel` | `string` | `'More information'` | Accessible name of the help icon. |
| `required` | `boolean` |  | Draws the shared `*` after the label. Visual only; a range input always has a value. |
| `optional` | `string \| boolean` |  | Marks the field optional after the label. `true` renders `(Optional)`; a string replaces it, such as `'(ไม่บังคับ)'`. |
| `aria-label` | `string` |  | The slider name when no `label` shows. |
| `aria-labelledby` | `string` |  | The id of the element that names the slider when no `label` shows. |
| `value` | `V` |  | The controlled value. A number is one thumb and a pair is a range. |
| `defaultValue` | `V` |  | The uncontrolled start value. A number is one thumb and a pair is a range. |
| `thumbLabels` | `readonly [start: string, end: string]` | `['start', 'end']` | Appended to the slider name for each thumb, so a screen reader hears "Price start" and "Price end". |
| `minStepsBetweenValues` | `number` |  | Smallest gap between the thumbs, in steps. Defaults to `0`. |
| `onValueChange` | `((value: WidenedSliderValue<V>) => void)` |  | Fires on every change while dragging or on each key press. |
| `onValueCommitted` | `((value: WidenedSliderValue<V>) => void)` |  | Fires once when a drag ends or a key press lands, and only if the value changed. |
| `min` | `number` | `0` | The lowest value. |
| `max` | `number` | `100` | The highest value. |
| `step` | `number` |  | Defaults to `1`. |
| `largeStep` | `number` |  | Page Up, Page Down and Shift+Arrow. Defaults to `10`. |
| `name` | `string` |  | Submits each thumb's value under this name. |
| `form` | `string` |  | The id of a `<form>` the hidden inputs belong to when the slider sits outside it. |
| `orientation` | `"horizontal" \| "vertical"` | `'horizontal'` | Vertical is a fixed 160px control. |
| `valueDisplay` | `"none" \| "text" \| "tooltip"` | `'tooltip'` | `'tooltip'` shows a bubble while a thumb is hovered, pressed or keyboard focused. `'text'` shows the value beside the track. `'none'` hides it. |
| `formatValue` | `((value: number) => string)` | `String` | The one formatter. It writes the tooltip, the value text and each thumb's `aria-valuetext`. A range joins two results with `' – '`. |
| `marks` | `readonly SliderMark[]` | `[]` | Ticks along the track. A mark with a `label` also draws the label. |
| `description` | `SliderText` |  | Text under the label, linked with `aria-describedby`. |
| `disabled` | `boolean` | `false` | Dims the slider and blocks changes. |
| `disabledMessage` | `SliderText` |  | Why the slider is disabled. Shown in the message slot with the disabled tone, in place of `message`, only while `disabled`. |
| `status` | `"error" \| "success" \| "warning"` |  | Picks the message tone and glyph. Without a message nothing shows. |
| `message` | `SliderText` |  | The message under the slider, toned by `status` and linked with `aria-describedby`. |
| `className` | `string` |  | Merged last with the token-aware `cn()`. The slider fills its container; set a width here. |
| `ref` | `Ref<HTMLDivElement>` |  | The root `div`. |