Skip to the content

ComponentsForm

Slider

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

Import

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

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 beside it, as in With input.

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.

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.

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.

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.

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.

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.

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.

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.

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

Keyboard
KeyBehaviour
โ†’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 UporPage DownIncreases or decreases the value by ten steps.
HomeorEndSets the minimum or the maximum.
TabMoves to the next handle, then out of the slider.

API

Slider

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

Slider props
PropTypeDefaultDescription
asChildboolean
defaultValuenumber[]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.
disabledbooleanDims the slider and ignores the pointer and the keyboard.
formstring
invertedbooleanWhether the value runs the other way along the track.
maxnumber100The highest value. 100 by default.
minnumber0The lowest value. 0 by default.
minStepsBetweenThumbsnumberThe fewest steps a range's handles keep between them. 0 by default.
namestringThe 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.
stepnumberThe amount each move changes the value by. 1 by default.
valuenumber[]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.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--backgroundbackground
--borderbackground
--cardbackground
--primarybackground
--ringoutline