Terracotta
GitHub
import {
  Slider,
  SliderLabel,
  SliderThumb,
  SliderTrack,
  SliderStateChild,
  useSliderState,
} from 'terracotta/slider';

Anatomy

<Slider>          {/* owns the value, min, max and step */}
  <SliderLabel/>  {/* optional; names the thumb */}
  <SliderTrack>   {/* press and drag to set the value */}
    <SliderThumb/>{/* role="slider", focusable */}
  </SliderTrack>
</Slider>

Put the thumb inside the track, so dragging the thumb works like dragging the track.

Examples

Basic

The thumb and the filled range are placed from percent(). The second slider has step={0.5}, largeStep={4} and an aria-valuetext formatter.

Placing the thumb

Terracotta moves the value, not the thumb. Read percent(), from 0 to 100, and place the thumb with it:

<Slider defaultValue={40}>
  {state => (
    <SliderTrack class="slider-track">
      <SliderThumb
        class="slider-thumb"
        style={{ 'inset-inline-start': `${state.percent()}%` }}
      />
    </SliderTrack>
  )}
</Slider>
.slider-track { position: relative; touch-action: none; }
.slider-thumb { position: absolute; translate: -50% -50%; inset-block-start: 50%; }

touch-action: none on the track stops a touch drag from scrolling the page.

Value text

When the number alone is not clear, getValueText sets aria-valuetext:

<Slider defaultValue={3} min={1} max={5} getValueText={value => `${value} of 5 stars`}>
  …
</Slider>

Controlled

const [volume, setVolume] = createSignal(40);

<Slider value={volume()} onChange={setVolume}>…</Slider>

Vertical

vertical makes the track run from bottom to top, and sets aria-orientation="vertical" on the thumb. Place the thumb with inset-block-end instead.

Without a visible label

Leave out SliderLabel and name the thumb directly:

<SliderThumb aria-label="Zoom" />

Several thumbs

Pass an array as the value, and render one SliderThumb per value with its index:

  • The thumbs cannot pass each other. Each thumb's aria-valuemin and aria-valuemax are the values of the thumbs beside it, and they update as those thumbs move.
  • Each thumb is in the tab sequence, in index order, wherever it is on screen.
  • SliderLabel names the group, which gets role="group". Give each thumb a name of its own, such as aria-label="Minimum price".
  • A press on the track moves the thumb nearest the pointer.
  • onChange receives the whole array. getValueText receives each thumb's value and index.
  • A starting value given out of order is sorted.
<Slider defaultValue={[200, 650]} max={1000} step={10}>
  {state => (
    <SliderTrack class="slider-track">
      <SliderThumb index={0} aria-label="Minimum price"
        style={{ 'inset-inline-start': `${state.percent(0)}%` }} />
      <SliderThumb index={1} aria-label="Maximum price"
        style={{ 'inset-inline-start': `${state.percent(1)}%` }} />
    </SliderTrack>
  )}
</Slider>

In a form

name adds a hidden input that carries the value, one per thumb:

<Slider defaultValue={40} name="volume">…</Slider>

State attributes

Element Attribute Present when
Slider tc-slider Always
SliderLabel tc-slider-label Always
SliderTrack tc-slider-track Always
SliderThumb tc-slider-thumb Always
Slider tc-vertical The slider is vertical
Slider, SliderTrack, SliderThumb tc-disabled The slider is disabled

Reading the state in code

Member Type Description
value() number The value of the first thumb.
values() number[] The values of every thumb.
min() / max() / step() number The range and step.
percent(index?) number Where a thumb's value sits in the range, from 0 to 100.
disabled() boolean Whether the slider is disabled.
setValue(value) (number) => void Sets the first thumb's value, snapped to the step and kept in range.
setThumbValue(index, value) (number, number) => void Sets one thumb's value, kept between its neighbours.
thumbMin(index) / thumbMax(index) number The range a thumb can move in.
increment(steps?, index?) / decrement(steps?, index?) (number?, number?) => void Moves a thumb by a number of steps, 1 by default.

You can reach this state as the render-prop argument on Slider, SliderTrack or SliderThumb, through <SliderStateChild>, or with useSliderState() inside any descendant component.

Keyboard

Handled on the thumb:

Key Action
→ / ↑ Increases the value by one step
← / ↓ Decreases the value by one step
Page Up / Page Down Changes the value by largeStep steps
Home / End Sets the lowest / highest value the thumb can take

API

<Slider>

Renders a <div> by default. Pass exactly one of defaultValue or value. Both take a number, or an array for several thumbs.

Prop Type Default Description
as ValidConstructor 'div' Element or component to render as.
defaultValue number | number[] none Initial value, uncontrolled.
value number | number[] none Current value, controlled.
min number 0 The lowest value.
max number 100 The highest value.
step number 1 The distance between allowed values.
largeStep number 10 Steps moved by Page Up and Page Down.
vertical boolean false Runs the slider from bottom to top.
disabled boolean false Blocks changes.
getValueText (value: number, index: number) => string none Text for each thumb's aria-valuetext.
name string none Submits the value with a form, one input per thumb.
onChange (value: number | number[]) => void none Called with the new value, in the same shape as value.
children JSX.Element | (state: SliderStateProperties) => JSX.Element none Contents, or a render prop.
…rest props of as none Forwarded to the rendered element.

<SliderLabel>

Renders a <label> by default. The thumb points aria-labelledby at it while it is mounted. Clicking it focuses the thumb. With several thumbs, the slider gets role="group" and the label names the group instead.

<SliderTrack>

Renders a <div> by default. Pressing it sets the value under the pointer and focuses the thumb. Dragging keeps setting it until the pointer is released. With several thumbs, the press moves the thumb nearest the pointer.

<SliderThumb>

Renders a <div> by default.

Prop Type Default Description
index number 0 Which value the thumb sets, with several thumbs.

Rendered attributes: role="slider", tabindex="0" (-1 while disabled), aria-valuenow, aria-valuemin and aria-valuemax (limited by the neighbouring thumbs), aria-valuetext when getValueText is set, aria-orientation="vertical" when vertical, aria-labelledby while a SliderLabel is mounted and there is one thumb, aria-disabled, tc-slider-thumb.

All three parts take as, ref, children (or a render prop) and forward the rest of their props.