# Slider

Adjust a number or a range with draggable thumbs and keyboard controls.

Use a slider to pick a number or a range along a scale, like a volume or a price range, by dragging a thumb or with the arrow keys.

<ComponentExample
  name="slider/basic"
  inspect
  caption="Drag the thumb, or focus it and use the arrow keys, to adjust Volume."
/>

## Installation

```bash
npm install @ui-sdk/vue @ui-sdk/core
```

## Composition

Use the following composition to build a `Slider`:

```text
Slider
├── SliderValue
└── SliderControl
    └── SliderTrack
        ├── SliderIndicator
        └── SliderThumb
```

A control handles presses around the track. Put a thumb on that track for each value.

- **Slider** holds the value and is a group around the thumbs.
- **SliderControl** is where presses land. Pressing it moves the nearest thumb and starts a drag. Give it some height around a thin track.
- **SliderTrack** is the rail the thumbs move along. Thumbs and the indicator are placed in percentages of it with inline styles, so give it a size. It's the positioned box.
- **SliderIndicator** fills the track up to a single thumb, or between the first and last thumb of a range.
- **SliderThumb** is what you drag. Give each one a size.
- **SliderValue** shows the formatted value, and a range joined with an en dash. It's optional.

Each thumb renders a visually hidden `<input type="range">` inside itself. That input is what takes focus, so
screen readers on phones adjust it with their own gestures. Style focus on the thumb with `:has(:focus-visible)`.
The slider also renders a hidden input that the form submits.

A single thumb is named by a `FieldLabel` in a `Field`, or by `aria-label` on the thumb. In a range, give
each thumb its own `aria-label`, like "Minimum price", and name the group with a `FieldLabel` or
`aria-label` on the slider.

## Usage

Import the parts and put them together:

```vue
<script setup lang="ts">
import {
  Slider,
  SliderValue,
  SliderControl,
  SliderTrack,
  SliderIndicator,
  SliderThumb,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Slider :default-value="50">
    <SliderValue />
    <SliderControl>
      <SliderTrack>
        <SliderIndicator />
        <SliderThumb aria-label="Volume" />
      </SliderTrack>
    </SliderControl>
  </Slider>
</template>
```

## Range

Pass an array for a range, then render one thumb per number. Give each thumb its `index`, from 0, and its own name, like "Minimum price". The indicator fills the space between the first and the last thumb, and the `FieldLabel` names the group.

```vue title="range.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Field>
    <Slider
      :default-value="[25, 50]"
      :step="5"
      :format="{ style: 'currency', currency: 'USD', maximumFractionDigits: 0 }"
    >
      <div data-slot="row">
        <FieldLabel>Price</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb :index="0" aria-label="Minimum price" />
          <SliderThumb :index="1" aria-label="Maximum price" />
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Move either end of Price. The thumbs stay in order.

## Multiple Thumbs

Any number of thumbs works the same way. They never pass each other, and a press on the track moves the nearest one.

```vue title="multiple.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"

const thumbs = ["Low", "Medium", "High"]
</script>

<template>
  <Field>
    <Slider
      :default-value="[10, 20, 70]"
      :step="10"
      :format="{ style: 'unit', unit: 'percent' }"
    >
      <div data-slot="row">
        <FieldLabel>Alert thresholds</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb
            v-for="(label, index) in thumbs"
            :key="label"
            :index="index"
            :aria-label="label"
          />
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Adjust Low, Medium and High independently, without moving one past another.

## Vertical

Set `orientation="vertical"` and give the slider a height. The bottom is the minimum, ArrowUp increases the value, and every part gets `data-orientation`, so `[data-orientation="vertical"]` styles the vertical axis.

```vue title="vertical.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
} from "@ui-sdk/vue/slider"

const channels = [
  { label: "Bass", value: 50 },
  { label: "Treble", value: 25 },
]
</script>

<template>
  <div data-slot="row">
    <Field v-for="channel in channels" :key="channel.label">
      <Slider orientation="vertical" :default-value="channel.value">
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
      </Slider>
      <FieldLabel>{{ channel.label }}</FieldLabel>
    </Field>
  </div>
</template>
```

Drag up or down, or use the arrow keys, to adjust Bass and Treble.

## Field

Put the slider in a `Field`. The label names the thumb, the description describes it, and `SliderValue` shows the value beside the label. Screen readers don't announce `SliderValue` as it changes, since the thumb already says its value.

```vue title="field.vue"
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Field>
    <Slider
      :default-value="0.3"
      :format="{ style: 'percent' }"
      :min="0"
      :max="1"
      :step="0.01"
    >
      <div data-slot="row">
        <FieldLabel>Volume</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb />
        </SliderTrack>
      </SliderControl>
    </Slider>
    <FieldDescription>How loud notifications play.</FieldDescription>
  </Field>
</template>
```

Click the Volume label to focus the thumb, then use the arrow keys to change the value.

## Disabled

Set `disabled`. A disabled slider can't be focused or moved, and isn't submitted. A disabled `<fieldset>` around it disables it too.

```vue title="disabled.vue"
<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Field>
    <Slider :default-value="50" disabled>
      <div data-slot="row">
        <FieldLabel>Brightness</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb />
        </SliderTrack>
      </SliderControl>
    </Slider>
    <FieldDescription>Managed by your organization.</FieldDescription>
  </Field>
</template>
```

Brightness keeps its value, but can't be focused or dragged.

## Controlled

Bind `v-model:value`, or pass `value` and handle `@value-change`, to control the slider. The value keeps its shape: a number stays a number and an array stays an array. Every change carries a [reason](#reasons), like `drag`, `track-press` or `step-key`, and `details.thumb` says which thumb moved. Call `details.cancel()` to keep the current value.

```vue title="controlled.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
import { ref } from "vue"
import { useLog } from "./log"

const log = useLog()
const value = ref([0.3, 0.7])
</script>

<template>
  <div data-slot="stack">
    <div data-slot="controls">
      <button type="button" @click="value = [0.3, 0.7]">Reset</button>
    </div>
    <Field>
      <Slider
        v-model:value="value"
        @value-change="
          (next, details) => log(`${next.join(' – ')} (${details.reason})`)
        "
        :min="0"
        :max="1"
        :step="0.1"
      >
        <div data-slot="row">
          <FieldLabel>Temperature</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb :index="0" aria-label="Minimum temperature" />
            <SliderThumb :index="1" aria-label="Maximum temperature" />
          </SliderTrack>
        </SliderControl>
      </Slider>
    </Field>
  </div>
</template>
```

Drag either Temperature thumb to log the range and its reason. Reset sets the value from outside.

## Step and Bounds

`min` and `max` set the bounds, 0 and 100 by default, and `step` sets how far one step moves, counted from `min`. Decimal steps avoid floating-point drift, so a 0.1 step can reach 0.3 cleanly. When the steps from `min` don't land on `max`, the highest step below it is the end.

```vue title="step.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <div data-slot="stack">
    <Field>
      <Slider
        :default-value="2010"
        :min="1990"
        :max="2030"
        :step="5"
        :format="{ useGrouping: false }"
      >
        <div data-slot="row">
          <FieldLabel>Year</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
        <div data-slot="ticks" aria-hidden="true">
          <span v-for="tick in 9" :key="tick" />
        </div>
      </Slider>
    </Field>
    <Field>
      <Slider :default-value="0.5" :min="0" :max="1" :step="0.05">
        <div data-slot="row">
          <FieldLabel>Opacity</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
        <div data-slot="ticks" aria-hidden="true">
          <span v-for="tick in 21" :key="tick" />
        </div>
      </Slider>
    </Field>
  </div>
</template>
```

Move Year in steps of five, and Opacity in steps of 0.05.

## Large Step

Arrow keys move one step. Page Up, Page Down and Shift with an arrow key move by `large-step`, which is ten steps unless you set it. Home and End go to the bounds.

```vue title="large-step.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Field>
    <Slider
      :default-value="100"
      :min="25"
      :max="400"
      :large-step="25"
      :format="{ style: 'unit', unit: 'percent' }"
    >
      <div data-slot="row">
        <FieldLabel>Zoom</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb />
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Focus Zoom and press an arrow key to move 1%, then Page Up, Page Down or Shift with an arrow key to move 25%.

## Distance Between Thumbs

Set `min-steps-between-thumbs` to keep neighbouring thumbs that many steps apart. A thumb stops at its neighbour, and Home and End move it only as far as its neighbour allows.

```vue title="min-steps.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"

const time = (minutes: number) =>
  `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}`
</script>

<template>
  <Field>
    <Slider
      :default-value="[540, 1020]"
      :min="0"
      :max="1440"
      :step="15"
      :min-steps-between-thumbs="16"
      :format="time"
    >
      <div data-slot="row">
        <FieldLabel>Opening hours</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb :index="0" aria-label="Opens" />
          <SliderThumb :index="1" aria-label="Closes" />
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Move either end of Opening hours. The thumbs stay at least four hours apart.

## Format and Locale

`format` takes `Intl.NumberFormat` options, like `{ style: "currency", currency: "USD" }` or `{ style: "percent" }`, or a function of the value. It sets what `SliderValue` shows and what screen readers announce for each thumb, while the value stays a plain number. `locale` sets separators, digits and symbol placement. Without it, the slider uses the page's `lang`, or the browser's language, once the page is interactive. Server markup uses English until then, so it never disagrees with the client.

```vue title="format.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"

const sliders = [
  {
    label: "Budget",
    defaultValue: 40,
    max: 100,
    step: 1,
    format: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
  },
  {
    label: "Opacity",
    defaultValue: 0.6,
    max: 1,
    step: 0.01,
    format: { style: "percent" },
  },
  {
    label: "Weight",
    defaultValue: 20,
    max: 50,
    step: 0.5,
    format: { style: "unit", unit: "kilogram" },
  },
  {
    label: "Duration",
    defaultValue: 90,
    max: 600,
    step: 15,
    format: (seconds: number) =>
      `${Math.floor(seconds / 60)} min ${seconds % 60} s`,
  },
  {
    label: "Preis",
    defaultValue: 40,
    max: 100,
    step: 1,
    format: { style: "currency", currency: "EUR" },
    locale: "de-DE",
  },
] satisfies {
  label: string
  defaultValue: number
  max: number
  step: number
  format: Intl.NumberFormatOptions | ((value: number) => string)
  locale?: string
}[]
</script>

<template>
  <div data-slot="stack">
    <Field v-for="slider in sliders" :key="slider.label">
      <Slider
        :default-value="slider.defaultValue"
        :max="slider.max"
        :step="slider.step"
        :format="slider.format"
        :locale="'locale' in slider ? slider.locale : undefined"
      >
        <div data-slot="row">
          <FieldLabel>{{ slider.label }}</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
      </Slider>
    </Field>
  </div>
</template>
```

Adjust Budget, Opacity, Weight, Duration and Preis to compare currency, a percentage, a unit, a custom function and German formatting.

## Saving on Commit

`@value-change` runs on every step of a drag. `@value-commit` runs once, when the interaction that changed the value ends: the pointer lifts, the key comes up, or a screen reader adjusts it. Use the commit callback to save once per interaction. A press that doesn't change the value commits nothing.

```vue title="commit.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
import { useLog } from "./log"

const log = useLog()
</script>

<template>
  <Field>
    <Slider
      @value-commit="
        (value, details) => log(`Saved ${value} (${details.reason})`)
      "
      :default-value="50"
    >
      <div data-slot="row">
        <FieldLabel>Brightness</FieldLabel>
        <SliderValue />
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb />
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Drag Brightness and release it, or press an arrow key. The console logs the value once, when you let go.

## Controller

A controller, from `createSliderController()`, moves the slider from anywhere with `setValue()`, reads it with `getValue()` and focuses a thumb with `focus()`. Values set through it are clamped, snapped to the step and sorted, and reported with the `imperative` reason.

```vue title="controller.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  createSliderController,
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
import { useLog } from "./log"

const log = useLog()
const volume = createSliderController<number>()
</script>

<template>
  <div data-slot="stack">
    <div data-slot="controls">
      <button type="button" @click="volume.setValue(0)">Mute</button>
      <button type="button" @click="volume.setValue(volume.getValue() + 10)">
        Louder
      </button>
      <button type="button" @click="log(`Volume is ${volume.getValue()}`)">
        Read
      </button>
      <button type="button" @click="volume.focus()">Focus</button>
    </div>
    <Field>
      <Slider
        :default-value="40"
        :controller="volume"
        @value-change="
          (value, details) => log(`Volume ${value} (${details.reason})`)
        "
      >
        <div data-slot="row">
          <FieldLabel>Volume</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
      </Slider>
    </Field>
  </div>
</template>
```

Use Mute, Louder and Focus to operate Volume from outside the slider. The console logs each change with the imperative reason, and Read logs the value.

## Form

A hidden input beside the slider is what the form sees. It takes the slider's `name`, `form` and `disabled`, so `FormData`, `form.reset()`, a disabled `<fieldset>` and the browser's restore all work.

- **Submit:** a single thumb submits its number under `name`, like `40`. A range submits `"start/end"`, like `200/800`. Without a name, it submits nothing.
- **Reset:** a form reset returns it to `default-value`, with the `reset` reason.
- **Join a form from elsewhere:** set `form` to the id of a form.

`required` adds `data-required`. A slider always has a value, so it never blocks a form.

```vue title="form.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
import { useId } from "vue"
import { useLog } from "./log"

const log = useLog()
const form = useId()

function onSubmit(event: Event) {
  const data = [...new FormData(event.currentTarget as HTMLFormElement)].map(
    ([name, value]) => `${name}=${value}`
  )
  log(`Submitted ${data.join(", ")}`)
}
</script>

<template>
  <div data-slot="stack">
    <form :id="form" data-slot="stack" @submit.prevent="onSubmit">
      <Field>
        <Slider name="price" :default-value="[200, 800]" :max="1000" :step="10">
          <div data-slot="row">
            <FieldLabel>Price</FieldLabel>
            <SliderValue />
          </div>
          <SliderControl>
            <SliderTrack>
              <SliderIndicator />
              <SliderThumb :index="0" aria-label="Minimum price" />
              <SliderThumb :index="1" aria-label="Maximum price" />
            </SliderTrack>
          </SliderControl>
        </Slider>
      </Field>
      <Field>
        <Slider name="rating" :default-value="3" :min="1" :max="5">
          <div data-slot="row">
            <FieldLabel>Minimum rating</FieldLabel>
            <SliderValue />
          </div>
          <SliderControl>
            <SliderTrack>
              <SliderIndicator />
              <SliderThumb />
            </SliderTrack>
          </SliderControl>
        </Slider>
      </Field>
    </form>
    <Field>
      <Slider
        name="distance"
        :form="form"
        :default-value="25"
        :max="100"
        :step="5"
        :format="{ style: 'unit', unit: 'kilometer' }"
      >
        <div data-slot="row">
          <FieldLabel>Distance</FieldLabel>
          <SliderValue />
        </div>
        <SliderControl>
          <SliderTrack>
            <SliderIndicator />
            <SliderThumb />
          </SliderTrack>
        </SliderControl>
      </Slider>
    </Field>
    <div data-slot="row">
      <button type="submit" :form="form" data-variant="solid">Save</button>
      <button type="reset" :form="form">Reset</button>
    </div>
  </div>
</template>
```

Adjust the filters and press Save to log the submitted values. Distance sits outside the form and joins it by id. Reset restores the defaults.

## Read-Only

Set `read-only` to keep the slider focusable and submitted, while drags, keys and screen reader adjustments leave its value alone.

```vue
<Field>
  <FieldLabel>Storage</FieldLabel>
  <Slider name="storage" :default-value="50" read-only>
    <SliderControl>
      <SliderTrack>
        <SliderIndicator />
        <SliderThumb />
      </SliderTrack>
    </SliderControl>
  </Slider>
  <FieldDescription>Set by your plan.</FieldDescription>
</Field>
```

## Custom Rendering

Every part takes a `render` slot, which receives the props to bind and the part's state, and the default slot receives the state too, like whether a thumb is being dragged, its `thumbValue` and its `percent` along the track. Here the thumb renders a `span`, its default slot adds a bubble with its value, and `SliderValue`'s default slot adds a percent sign.

```vue title="render.vue"
<script setup lang="ts">
import { Field, FieldLabel } from "@ui-sdk/vue/field"
import {
  Slider,
  SliderControl,
  SliderIndicator,
  SliderThumb,
  SliderTrack,
  SliderValue,
} from "@ui-sdk/vue/slider"
</script>

<template>
  <Field>
    <Slider :default-value="60">
      <div data-slot="row">
        <FieldLabel>Brightness</FieldLabel>
        <SliderValue v-slot="{ state }">{{ state.text }}%</SliderValue>
      </div>
      <SliderControl>
        <SliderTrack>
          <SliderIndicator />
          <SliderThumb>
            <template #render="{ props, children }">
              <span v-bind="props"><component :is="children" /></span>
            </template>
            <template #default="{ state }">
              <span data-slot="output">{{ state.thumbValue }}</span>
            </template>
          </SliderThumb>
        </SliderTrack>
      </SliderControl>
    </Slider>
  </Field>
</template>
```

Focus or drag the Brightness thumb to show its value above it.

See [Parts and Composition](/docs/vue/parts-and-composition) for the rendering contract and [State](#state) for each part's fields.

## Styling

Parts are unstyled, apart from the positions they need: thumbs and the indicator get `position: absolute` and their place along the track in percent, as inline styles. Give the control some height around a thin track, the track a size and a background, and each thumb a size. The thumb's range input is what takes focus, so draw the focus ring on the thumb with `:has(:focus-visible)`, and style a drag with `data-dragging`.

Each part has a `data-slot`, such as `slider-thumb`, and its state attributes are listed under [Data Attributes](#data-attributes).

The indicator and thumbs also write CSS variables, for anything else that should follow them, like a label above a thumb.

### SliderIndicator
- `--slider-indicator-start`: Where the indicator starts along the track, as a percentage. 0% for a single thumb.
- `--slider-indicator-size`: How much of the track the indicator covers, as a percentage.

### SliderThumb
- `--slider-thumb-position`: Where the thumb sits along the track, as a percentage from the start. Use it for a label that follows the thumb.

## Data Attributes

### Slider
`[data-slot="slider"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-dragging`: Present while a thumb is being dragged.

### SliderValue
`[data-slot="slider-value"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-dragging`: Present while a thumb is being dragged.

### SliderControl
`[data-slot="slider-control"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-dragging`: Present while a thumb is being dragged.

### SliderTrack
`[data-slot="slider-track"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-dragging`: Present while a thumb is being dragged.

### SliderIndicator
`[data-slot="slider-indicator"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-dragging`: Present while a thumb is being dragged.

### SliderThumb
`[data-slot="slider-thumb"]`

- `data-orientation` (`horizontal`, `vertical`): The slider's axis.
- `data-disabled`: Present when disabled, by its own prop or a `<fieldset>`.
- `data-index` (`0`, `1`): Which value the thumb shows, from 0.
- `data-dragging`: Present while this thumb is being dragged.

## API Reference

Props are the same as below, and callbacks are events.

### Slider

- `value?` (`Value`): The value: a number for one thumb, or an array with one number per thumb, like `[20, 80]` for a range. Pass it to control the slider.
- `defaultValue?` (`Value`): The value at first, when uncontrolled. A form reset returns to it.
- `@value-change` (`((value: Value, details: SliderValueChangeDetails) => void)`): Runs while the value changes: on every step of a drag or a key. `details.thumb` says which thumb moved, and `details.cancel()` stops it.
- `@value-commit` (`((value: Value, details: SliderValueCommitDetails) => void)`): Runs once an interaction that changed the value ends: the pointer lifts, the key comes up, or a screen reader adjusts it. For saving.
- `min?` (`number`): The lowest value.
- `max?` (`number`): The highest value. When the steps from `min` don't land on it, the highest step below it is the end.
- `step?` (`number`): How far one step moves, from `min`.
- `largeStep?` (`number`): How far Page Up, Page Down and Shift with an arrow key move.
- `minStepsBetweenThumbs?` (`number`): How many steps apart neighbouring thumbs stay. Thumbs never pass each other.
- `orientation?` (`"horizontal" | "vertical"`): The slider's axis. Vertical sliders grow upward.
- `disabled?` (`boolean`): Disables it like a native control: thumbs can't be focused or moved, and nothing is submitted.
- `readOnly?` (`boolean`): Keeps it focusable and submitted, but stops drags, keys and screen reader adjustments from changing it.
- `required?` (`boolean`): Marks it required, with `data-required`. A slider always has a value, so it never blocks a form.
- `name?` (`string`): The name it submits under: the number, or a range as `"start/end"`. Without one, it submits nothing.
- `form?` (`string`): The id of a form to belong to, when it isn't inside that form.
- `format?` (`NumberFormat`): How values read, for screen readers and `SliderValue`: `Intl.NumberFormat` options, like `{ style: "currency", currency: "EUR" }`, or a function of the value.
- `locale?` (`string`): The locale values are formatted in. Defaults to the page's `lang`, or the browser's language without one, once the page is interactive. Server markup uses English until then, so it never disagrees with the client.
- `controller?` (`{ getValue: () => Value; setValue(value: Value, options?: SliderChangeOptions): boolean; focus: (thumb?: number) => void; }`): Moves the slider from anywhere. Create one with `createSliderController()`.
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.

### SliderThumb

- `index?` (`number`): Which value the thumb shows, from 0. Give each thumb of a range its own.

### State

What render slots and default slots receive. Every part gets the slider's state.

- `value` (`number | number[]`): The value: a number, or one number per thumb.
- `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
- `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
- `required` (`boolean`): Whether it's marked required.
- `dragging` (`boolean`): Whether a thumb is being dragged.
- `orientation` (`"horizontal" | "vertical"`): The slider's axis.

Thumbs add their own fields, and have their own value, `thumbValue`, in place of the whole `value`, so a thumb renders only when its own value moves:

- `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
- `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
- `required` (`boolean`): Whether it's marked required.
- `dragging` (`boolean`): Whether a thumb is being dragged.
- `orientation` (`"horizontal" | "vertical"`): The slider's axis.
- `index` (`number`): Which value the thumb shows.
- `thumbValue` (`number`): The thumb's own value.
- `percent` (`number`): Where the thumb sits, from 0 to 100.

`SliderValue` adds the formatted text:

- `value` (`number | number[]`): The value: a number, or one number per thumb.
- `disabled` (`boolean`): Whether it's disabled, by itself or by a `<fieldset>`.
- `readOnly` (`boolean`): Whether it's read-only: it stays focusable and submitted, but drags and keys don't change it.
- `required` (`boolean`): Whether it's marked required.
- `dragging` (`boolean`): Whether a thumb is being dragged.
- `orientation` (`"horizontal" | "vertical"`): The slider's axis.
- `text` (`string`): The formatted value, a range joined with an en dash.

### Reasons

`details.reason` says why the value changed.

- `track-press` (`PointerEvent`): A press on a slider's track or an area's surface moved the nearest thumb there.
- `drag` (`PointerEvent`): A slider thumb or a resize handle was dragged, or an item was dragged to a new place in a list.
- `step-key` (`KeyboardEvent`): Arrow, Page Up, Page Down, Home or End stepped a slider or a number input.
- `accessibility-adjust` (`Event`): A screen reader's adjust gesture, like a swipe up or down on a slider.
- `reset` (`none`): A form reset.
- `restore` (`none`): The browser restored state, like after going back, or a saved value was read back from storage.
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.

### Controller

Create one with `createSliderController()` and pass it as `controller`.

`setValue()` takes an optional `{ reason, event }`, runs the change callback like a drag, and returns `false` when the change is canceled.

- `getValue()` (`() => Value`): The value: a number, or one number per thumb.
- `setValue(value, options?)` (`(value: Value, options?: SliderChangeOptions | undefined) => boolean`): Sets the value, as a user would. It's clamped, snapped to the step and sorted.
- `focus(thumb?)` (`(thumb?: number | undefined) => void`): Moves focus to a thumb, the first by default.

### Events

Events get the new value first, then the details. `v-model` only updates when the change event wasn't canceled.

| Binding         | On       | Receives                                                                                |
| --------------- | -------- | --------------------------------------------------------------------------------------- |
| `v-model:value` | `Slider` | The value: a number, or one number per thumb                                            |
| `@value-change` | `Slider` | `(value, details)`: the new value, the [reason](#reasons) and `details.thumb`           |
| `@value-commit` | `Slider` | `(value, details)`: the value once the interaction ends, its reason and `details.thumb` |

## Accessibility

The slider follows the [WAI-ARIA slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/), and the [multi-thumb slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/) for ranges. Each thumb is a native range input, named by a `FieldLabel` or its own `aria-label`, and the slider is a group around them.

### Keyboard

Focus a thumb to use these keys. Tab moves through the thumbs in order.

| Key                                        | Moves the thumb                                                       |
| ------------------------------------------ | --------------------------------------------------------------------- |
| ArrowRight or ArrowUp                      | One `step` up. In right to left, ArrowRight moves down.               |
| ArrowLeft or ArrowDown                     | One `step` down. In right to left, ArrowLeft moves up.                |
| Page Up, Page Down, or Shift with an arrow | By `large-step`, ten steps by default.                                |
| Home or End                                | To the minimum or maximum, or as far as a neighbouring thumb lets it. |

### Tested Behavior

Every line below is an automated test.

**Keyboard**

- Starts at its default value.
- ArrowRight and ArrowUp increase it by one step.
- ArrowLeft and ArrowDown decrease it by one step.
- Page Up and Page Down move by the large step.
- Shift with an arrow key moves by the large step.
- Home and End go to the minimum and the maximum.
- A value never goes past the bounds.
- Tab moves through the thumbs in order.
- Home and End move a range thumb as far as its neighbour.
- A range thumb stops at its neighbour.
- A canceled change keeps the value.
- A controlled slider follows its value.

**Pointer**

- Pressing the track moves the thumb there.
- Pressing the track focuses the thumb.
- Pressing a range's track moves the nearest thumb.
- Dragging a thumb follows the pointer.
- Grabbing a thumb off center doesn't move it.
- Dragging past the end stops at the bound.
- Dragging continues outside the slider.
- Dragging right from stacked thumbs moves the upper one.
- The value commits once, when the pointer lifts.
- A screen reader's adjustment reaches the form as one input and one change.
- A drag sends input as it moves, and change once when the pointer lifts.
- Pressing a thumb without moving commits nothing.
- A press on the track commits.
- The thumb sits at its value along the track.

**Vertical**

- It says it's vertical.
- ArrowUp increases it and ArrowDown decreases it.
- The bottom is the minimum.
- Dragging up increases it.

**Value and state**

- The value part shows the value, and a range joined with a dash.
- The value reads in the format and the locale.
- A disabled slider can't be focused or changed.
- Its parts say when it's dragged.
- A single slider submits its value under its name.

Runs in React, Vue and web components.

Beyond the pattern:

- A dragged range thumb stops at its neighbour, as the APG's multi-thumb slider says.
- Each range thumb's own range ends at its neighbour.
- A form reset returns every slider to its default.
- Holding an arrow key commits once, when the key comes up.
- Dragging left from stacked thumbs moves the lower one.
- Pressing the track beside stacked thumbs moves the one on that side.
- A key moves a thumb as close to its neighbour as the minimum distance allows.
- Touch drags on the control never scroll the page.
- A range submits one start/end value.
- Clicking a range's label focuses its first thumb.

Runs in React, Vue and web components.

Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:

- Announcements while moving to a slider with a keyboard.
- A range thumb announces how far it can go and its formatted value.

Runs in React, Vue and web components.
