# Slider

A track and one or more thumbs, for a number whose bounds are known in advance.

Slider picks a number, or a range of them, from a bounded interval, with one
thumb per value. Useful when the bounds matter more than the exact figure; where
the user needs to type a precise number, use [`Input`](/components/input).

::component-preview{name="SliderPreview"}
```vue
<template>
  <Slider.Root
    v-model="volume"
    class="w-64"
    :options="{ min: 0, max: 100, step: 1, largeStep: 10 }"
  >
    <div class="mb-3 flex items-baseline justify-between">
      <span class="type-component-lg text-primary-solid">Volume</span>
      <Slider.Value
        class="type-component-sm -number text-primary-muted tabular-nums"
      />
    </div>

    <Slider.Control
      class="flex h-8 w-full items-center px-[0.46875rem] [--mirror-slider-cursor:grab]"
    >
      <Slider.Track
        class="rounded-component-round bg-primary-subtle data-[disabled=true]:bg-disabled-subtle w-full transition-all duration-100 ease-linear"
      >
        <Slider.Indicator
          class="rounded-component-round bg-primary-solid data-[disabled=true]:bg-disabled-solid transition-colors duration-100 ease-linear"
        />
      </Slider.Track>

      <Slider.Thumb
        aria-label="Volume"
        class="group/thumb absolute [--mirror-slider-thumb-size:0.9375rem] focus-visible:outline-none"
      >
        <span
          class="rounded-component-lg bg-primary-solid group-data-[disabled=true]/thumb:bg-disabled-solid absolute top-1/2 left-1/2 size-[0.9375rem] -translate-x-1/2 -translate-y-1/2 outline-4 outline-transparent transition-all duration-100 ease-linear group-focus-visible/thumb:outline-4 group-focus-visible/thumb:outline-[color:var(--app-color-focus-outline)] group-data-[dragging=true]/thumb:size-[1.0625rem] group-data-[dragging=true]/thumb:outline-none!"
        />
      </Slider.Thumb>
    </Slider.Control>
  </Slider.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Slider } from '@maas/mirror/vue'

const volume = ref(60)
</script>
```
::

## Usage guidelines

- The positioning maths reads `--mirror-slider-thumb-size`, so size the thumb
  with that variable; a `width` alone leaves both ends off by half a thumb.
- An array value is clamped and sorted ascending before anything derives from
  it, so a model that arrives out of order still renders on the track.
- Make sure the slider has a label, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on each thumb. A range slider names its two thumbs “start range”
  and “end range” on top of that.

## Anatomy

Assemble the parts, one `Slider.Thumb` per value.

::component-anatomy
---
parts:
  - name: Slider.Root
    required: true
    description: 'Renders a <div role="group"> plus one visually hidden <input type="range"> per thumb.'
    children:
      - name: Slider.Value
        description: Renders an <output> with the formatted value.
      - name: Slider.Control
        required: true
        description: Owns the pointer interaction.
        children:
          - name: Slider.Track
            required: true
            description: The box the indicator and the thumbs position themselves against, and the box the pointer maths measures.
            children:
              - name: Slider.Indicator
                description: Sized to the filled portion.
              - name: Slider.Thumb
                required: true
                description: One per value.
---
::

```vue
<script setup lang="ts">
import { Field, Label, Slider } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'volume' }">
    <Label :native-label="false">Volume</Label>
    <Slider.Root v-model="volume" :options="{ min: 0, max: 100, step: 1 }">
      <Slider.Value />
      <Slider.Control>
        <Slider.Track>
          <Slider.Indicator />
          <Slider.Thumb />
        </Slider.Track>
      </Slider.Control>
    </Slider.Root>
  </Field.Root>
</template>
```

## Examples

### Range values

An array value makes it a range slider, and the anatomy does not change: one
`Slider.Thumb` per entry.

```vue
<template>
  <Slider.Root v-model="range" :options="{ min: 0, max: 100 }">
    <Slider.Control>
      <Slider.Track>
        <Slider.Indicator />
        <Slider.Thumb v-for="(_, index) in range" :key="index" />
      </Slider.Track>
    </Slider.Control>
  </Slider.Root>
</template>

<script setup lang="ts">
const range = ref([20, 80])
</script>
```

`options.minStepsBetweenThumbs` widens the bound between neighbours into a gap,
counted in steps rather than in units.

```vue
<template>
  <Slider.Root
    v-model="range"
    :options="{ step: 5, minStepsBetweenThumbs: 2 }"
  />
</template>
```

`options.thumbCollisionBehavior` decides what a dragged thumb does when it
reaches its neighbour. It pushes the neighbour along by default, `swap` hands
the drag to the neighbour it passed, and `none` stops it dead. The keyboard
always stops at the neighbour, whatever the setting.

```vue
<template>
  <Slider.Root
    v-model="range"
    :options="{ thumbCollisionBehavior: 'swap' }"
  />
</template>
```

### Step and large step

Values snap to `min + n * step` and clamp to the bounds, on the keyboard and
under the pointer alike, and a fractional `options.step` keeps its precision.

```vue
<template>
  <Slider.Root v-model="rate" :options="{ min: 0, max: 1, step: 0.1 }" />
</template>
```

`options.largeStep` is the granularity of `PageUp`, `PageDown` and `Shift` plus
an arrow, so set it only where the coarse jump should not be ten notches.

```vue
<template>
  <Slider.Root
    v-model="year"
    :options="{ min: 1900, max: 2100, step: 1, largeStep: 25 }"
  />
</template>
```

### Orientation and direction

`options.orientation` decides the axis the track lays out on and the axis the
pointer is read from, and a vertical slider is read bottom-up.

```vue
<template>
  <Slider.Root v-model="volume" :options="{ orientation: 'vertical' }">
    <Slider.Control>
      <Slider.Track>
        <Slider.Indicator />
        <Slider.Thumb />
      </Slider.Track>
    </Slider.Control>
  </Slider.Root>
</template>
```

`options.dir` mirrors the horizontal axis for both the pointer and the arrow
keys, and is rendered onto the root so the parts’ logical properties follow it.
Unset, it follows a `DirectionProvider` or the surrounding `dir` attribute, as
described in [Composition](/components/composition).

### Formatting the value

Sliders render the raw number. `options.format` and `options.locale` run it
through `Intl.NumberFormat`, for `Slider.Value` and for what each thumb
announces alike, so the readout and the announcement never drift apart.

```vue
<template>
  <Slider.Root
    v-model="rate"
    :options="{
      min: 0,
      max: 1,
      step: 0.01,
      format: { style: 'percent' },
      locale: 'en-GB',
    }"
  >
    <Slider.Value />
  </Slider.Root>
</template>
```

A range renders both entries joined by an en dash. If you want something else
entirely, take the default slot, which hands over the entries one by one as
well as joined.

```vue
<template>
  <Slider.Value v-slot="{ formatted, formattedValues }">
    {{ formattedValues.join(' to ') || formatted }}
  </Slider.Value>
</template>
```

### Naming the thumbs

A range slider announces `20 start range` and `80 end range`, so the two thumbs
are told apart under one field label. `options.getAriaLabel` and
`options.getAriaValueText` replace that, and both receive the thumb’s index, so
one function can still name each thumb differently.

```vue
<template>
  <Slider.Root
    v-model="range"
    :options="{
      getAriaLabel: (index) => (index === 0 ? 'Minimum' : 'Maximum'),
      getAriaValueText: (formatted) => `${formatted} pounds`,
    }"
  />
</template>
```

### Disabling one thumb

`options.disabled` blocks the whole control. `disabled` on a `Slider.Thumb`
blocks that thumb alone: it leaves the tab order, ignores the keyboard and is
skipped when a press looks for the nearest thumb.

```vue
<template>
  <Slider.Root v-model="range">
    <Slider.Control>
      <Slider.Track>
        <Slider.Indicator />
        <Slider.Thumb disabled />
        <Slider.Thumb />
      </Slider.Track>
    </Slider.Control>
  </Slider.Root>
</template>
```

### Reacting to changes

`update:modelValue` fires on every change, a drag included, while `valueCommit`
fires once the interaction ends, so render from the first and persist from the
second.

```vue
<template>
  <Slider.Root
    v-model="volume"
    @value-commit="save"
    @dragging="scrubbing = $event"
  />
</template>
```

`valueCommit` fires on the release of a movement key that moved something, so a
key pressed against a bound commits nothing. `dragging` brackets a pointer drag
and never fires for a keyboard change.

### Decorating a thumb

The thumb’s position lives in a component-local variable, so take the
`percentage` or `value` slot prop and put the bubble inside the thumb it belongs
to.

```vue
<template>
  <Slider.Root v-model="volume">
    <Slider.Control>
      <Slider.Track>
        <Slider.Indicator />
        <Slider.Thumb v-slot="{ value }">
          <span class="bubble">{{ value }}</span>
        </Slider.Thumb>
      </Slider.Track>
    </Slider.Control>
  </Slider.Root>
</template>
```

### Inside a form

`Slider.Root` renders one visually hidden range input per thumb, each with
`name`, `min`, `max`, `step` and the current value, so
[the form sees it](/components/form-integration#form-participation).

```vue
<template>
  <form @submit.prevent="submit">
    <Slider.Root
      v-model="range"
      :options="{ name: 'range', required: true }"
    />
  </form>
</template>
```

`options.form` points the hidden inputs at a form by id, which is how a slider
rendered outside the `<form>` still submits with it.

```vue
<template>
  <Slider.Root
    v-model="range"
    :options="{ name: 'range', form: 'settings' }"
  />
</template>
```

The thumbs are the only tab stops, so they report focus to a `Field` and
blurring one marks the field touched.

### Reaching the slider from anywhere

Give the root an `id` and `useMirrorSlider(id)` reads and writes that slider
from anywhere in the app.

```vue
<template>
  <Button @click="setValue(0)">Mute</Button>
</template>

<script setup lang="ts">
const { value, percentages, dragging, setValue, setValueAt, focusThumb } =
  useMirrorSlider(SliderId.Volume)
</script>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorSlider(id)` as
the programmatic API. Every part takes `id` to resolve a slider it is not nested
inside.

### `Slider.Root`

Renders a `<div role="group">` plus one visually hidden `<input type="range">`
per thumb.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: '[The instance ID](#reaching-the-slider-from-anywhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: 'The value, [an array for a range](#range-values). `v-model`.'
      - label: 'number | number[] | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: 'Initial value when [uncontrolled](/components/composition#controlled-and-uncontrolled).'
      - label: 'number | number[]'
      - label: options.min
        plaintext: true
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: SliderOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: min
        description: Lower bound of the range.
      - label: number
      - label: '0'
  - items:
      - label: max
        description: Upper bound of the range.
      - label: number
      - label: '100'
  - items:
      - label: step
        description: 'Granularity. Values [snap](#step-and-large-step) to `min + n * step`.'
      - label: number
      - label: '1'
  - items:
      - label: largeStep
        description: 'Granularity of the [coarse jump](#step-and-large-step).'
      - label: number
      - label: step * 10
  - items:
      - label: minStepsBetweenThumbs
        description: 'Minimum [gap between adjacent thumbs](#range-values), in steps.'
      - label: number
      - label: '0'
  - items:
      - label: thumbCollisionBehavior
        description: 'What a dragged thumb does [at its neighbour](#range-values).'
      - label: '''push'' | ''swap'' | ''none'''
      - label: '''push'''
  - items:
      - label: orientation
        description: 'The [track axis](#orientation-and-direction).'
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
  - items:
      - label: dir
        description: 'Direction for horizontal sliders. [Inherited](/components/composition#text-direction) when unset.'
      - label: '''ltr'' | ''rtl'''
      - label: inherited
        plaintext: true
  - items:
      - label: locale
        description: Locale for `Intl.NumberFormat`.
      - label: Intl.LocalesArgument
      - label: the runtime locale
        plaintext: true
  - items:
      - label: format
        description: '[Formatting](#formatting-the-value) for `Slider.Value` and for what each thumb announces.'
      - label: Intl.NumberFormatOptions
      - label: undefined
  - items:
      - label: getAriaLabel
        description: '[Names](#naming-the-thumbs) every thumb by index.'
      - label: '(index: number) => string'
        escape: true
      - label: undefined
  - items:
      - label: getAriaValueText
        description: 'Replaces [what every thumb announces](#naming-the-thumbs).'
      - label: '(formatted: string, value: number, index: number) => string'
        escape: true
      - label: undefined
  - items:
      - label: name
        description: 'Form field name, on [each hidden input](#inside-a-form).'
      - label: string
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: form
        description: The `id` of the form, when rendered outside it.
      - label: string
      - label: undefined
  - items:
      - label: disabled
        description: Blocks changes and removes every thumb from the tab order.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: readOnly
        description: Blocks changes, keeps focus.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: required
        description: Marks the hidden inputs required.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The value changes, including during a drag.
      - label: 'number | number[]'
  - items:
      - label: valueCommit
        description: 'The [interaction ends](#reacting-to-changes).'
      - label: 'number | number[]'
  - items:
      - label: dragging
        description: A pointer drag starts or ends.
      - label: boolean
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus the three below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The current value, in the shape it was given.
      - label: 'number | number[]'
  - items:
      - label: percentages
        description: Each thumb’s position, `0` to `100`.
      - label: number[]
  - items:
      - label: dragging
        description: Mirrors `data-dragging`.
      - label: boolean
---
::

### `Slider.Control`

Renders a `<div>` and owns the pointer interaction: a pointerdown anywhere on it
captures the pointer, moves the nearest thumb to it, focuses that thumb and
starts a drag. It sits apart from `Slider.Track` so the hit area can be larger
than the line.

#### Slot props

The field state set, plus `dragging: boolean`.

### `Slider.Track`

Renders a `<div>`, the box the indicator and the thumbs position themselves
against and the one the pointer maths measures.

#### Slot props

The field state set.

### `Slider.Indicator`

Renders a `<div>` sized to the filled portion, which runs from `options.min` to
the value for a single thumb, and between the lowest and highest thumb for a
range.

#### Slot props

The field state set.

### `Slider.Thumb`

Renders a `<span role="slider">`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the slider this belongs to.
      - label: string
      - label: injected
        plaintext: true
  - items:
      - label: elementId
        description: The thumb’s own DOM ID.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: index
        description: 'Which entry of an [array value](#range-values) this thumb drives.'
      - label: number
      - label: its document position
        plaintext: true
  - items:
      - label: disabled
        description: 'Blocks [this thumb](#disabling-one-thumb) alone.'
      - label: boolean
      - label: 'false'
---
::

#### Slot props

The field state set, plus the five below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: This thumb’s value.
      - label: number
  - items:
      - label: percentage
        description: This thumb’s position, `0` to `100`.
      - label: number
  - items:
      - label: index
        description: Mirrors `data-index`.
      - label: number
  - items:
      - label: active
        description: Mirrors `data-active`.
      - label: boolean
  - items:
      - label: disabled
        description: Mirrors `data-disabled`, this thumb’s own or the whole control’s.
      - label: boolean
---
::

### `Slider.Value`

Renders an `<output aria-live="off">` whose `for` names every thumb it reports.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the slider this belongs to.
      - label: string
      - label: injected
        plaintext: true
---
::

#### Slot props

The field state set, plus the four below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The current value, in the shape it was given.
      - label: 'number | number[]'
  - items:
      - label: values
        description: The value as an array, whatever its shape.
      - label: number[]
  - items:
      - label: formatted
        description: The formatted value.
      - label: string
  - items:
      - label: formattedValues
        description: One formatted string per thumb.
      - label: string[]
---
::

### Composable

`useMirrorSlider(id)` reaches a slider from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: value
        description: The value, in the shape it was given.
      - label: ComputedRef<SliderModelValue>
        escape: true
  - items:
      - label: values
        description: The value as an array, whatever its shape.
      - label: ComputedRef<Array<number>>
        escape: true
  - items:
      - label: percentages
        description: Each thumb’s position, `0` to `100`.
      - label: ComputedRef<Array<number>>
        escape: true
  - items:
      - label: dragging
        description: Mirrors `data-dragging`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: activeIndex
        description: The thumb being dragged or focused.
      - label: 'ComputedRef<number | null>'
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: readOnly
        description: Mirrors `data-readonly`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: setValue
        description: Sets the whole value.
      - label: '(next: SliderModelValue) => void'
        escape: true
  - items:
      - label: setValueAt
        description: Sets one thumb’s value.
      - label: '(index: number, next: number) => void'
        escape: true
  - items:
      - label: focusThumb
        description: Focuses one thumb.
      - label: '(index: number) => void'
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_slider_context
        description: Any `Slider` part rendered outside `Slider.Root` with no `id` of its own.
  - items:
      - label: invalid_slider_range
        description: '`options.min` is greater than or equal to `options.max`, or `options.step` is not positive.'
  - items:
      - label: invalid_thumb_index
        description: A `Slider.Thumb` resolved an index with no matching entry in an array value.
---
::

## Accessibility

The root is a `role="group"`. Each thumb is a `role="slider"` with
`aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-orientation` and,
while read-only, `aria-readonly`, and each is its own tab stop rather than part
of a roving-focus group.

`aria-valuetext` says which end of a range each thumb holds, and carries the
formatted value wherever `options.format` is set. `options.getAriaValueText`
replaces it, and `options.getAriaLabel` names the thumbs where the field label
is not enough.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: ArrowRight
      - label: Increase by `options.step`. Decreases in an RTL horizontal slider.
        plaintext: true
  - items:
      - label: ArrowLeft
      - label: Decrease by `options.step`. Increases in an RTL horizontal slider.
        plaintext: true
  - items:
      - label: ArrowUp
      - label: Increase by `options.step`. Never mirrors.
        plaintext: true
  - items:
      - label: ArrowDown
      - label: Decrease by `options.step`. Never mirrors.
        plaintext: true
  - items:
      - label: '`Shift` + any arrow'
        plaintext: true
      - label: Increase or decrease by `options.largeStep`, mirroring the same way the plain arrow does.
        plaintext: true
  - items:
      - label: '`PageUp` / `PageDown`'
        plaintext: true
      - label: Increase or decrease by `options.largeStep`.
        plaintext: true
  - items:
      - label: '`Home` / `End`'
        plaintext: true
      - label: Jump to `options.min` or `options.max`.
        plaintext: true
---
::

Every key above is live in both orientations, and releasing one that moved the
value emits `valueCommit`. A range thumb stops at its neighbour on the keyboard
whatever `options.thumbCollisionBehavior` says, since a key press has no direction to
carry a neighbour in.
