Skip to content
txbrendan/ui

Slider

beta

A track with a thumb people drag or move with arrow keys to pick a number, or two thumbs for a range, with a label, tones, and sizes.

Preview

tone

Neutral
30
Brand
42
Danger
54
Success
66
Warning
78

size

Small
Medium
Large

range

Price
$40 – $140

states

At the start
0
At the end
100
Disabled
40

vertical

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/slider

Use it for

  • Picking a number where the rough amount matters more than the exact one, like volume or brightness.
  • A range with two thumbs, like a price filter. Pass an array: defaultValue={[20, 80]}.
  • Give it a label, and add showValue when people need the number. Use format for units, like dollars.
  • Name each thumb of a range with thumbLabels, like ['Lowest price', 'Highest price'].

Avoid it for

  • An exact number, like a quantity or an age. Use an input with type='number'.
  • A choice from a few named options. Use a radio group.
  • A setting that is on or off. Use a switch.
  • A value that only shows progress. Use a progress bar.

Props

  • label

    Visible text above the track. Without it, TypeScript requires aria-label or aria-labelledby.

    ReactNode

  • aria-label

    Names the slider when there is no visible label. It goes on each thumb.

    string

  • aria-labelledby

    The id of visible text elsewhere that names the slider. It goes on each thumb.

    string

  • showValue

    Shows the value as text at the end of the label row. A range shows both ends, like '20 – 80'.

    boolean · default false

  • thumbLabels

    A name for each thumb of a range slider, in order. It replaces the label as each thumb's name.

    readonly string[]

  • tone

    Color of the filled part and the thumb's ring.

    "neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"

  • size

    Track and thumb size, and label text size. The row is as tall as a button of the same size.

    "sm" | "md" | "lg" · default "md"

  • value

    Controlled value. An array makes a range, with one thumb per value. Use it with onValueChange.

    number | number[]

  • defaultValue

    Starting value when you do not control it. An array makes a range.

    number | number[]

  • onValueChange

    Called on every move, while dragging too.

    (value: number | number[], details) => void

  • onValueCommitted

    Called once when a drag or key press ends. Save here, not in onValueChange.

    (value: number | number[], details) => void

  • min

    The lowest value.

    number · default 0

  • max

    The highest value.

    number · default 100

  • step

    How far one arrow key press moves. Values snap to it.

    number · default 1

  • largeStep

    How far Page Up, Page Down, or Shift with an arrow moves.

    number · default 10

  • minStepsBetweenValues

    For a range, the fewest steps the two thumbs can be apart.

    number · default 0

  • format

    How the value reads, as text and to screen readers, like { style: 'currency', currency: 'USD' }.

    Intl.NumberFormatOptions

  • orientation

    Vertical sliders are 10rem tall. className='h-64' changes it.

    "horizontal" | "vertical" · default "horizontal"

  • disabled

    Blocks moving it and removes it from the tab order. It dims.

    boolean · default false

  • name

    Form field name. The value is sent with the form.

    string

  • className

    Classes for the root. Later classes win, so className='max-w-xs' narrows it.

    string

Accessibility

  • Each thumb holds a native input with type="range", so it has role="slider" and the value is sent with the form.
  • Arrow keys move by step. Page Up and Page Down, or Shift with an arrow, move by largeStep. Home and End go to min and max.
  • Each thumb is a tab stop. Tab moves from the first thumb of a range to the second.
  • TypeScript requires a visible label, aria-label, or aria-labelledby, so each thumb has a name.
  • In server HTML, only aria-label, aria-labelledby, and thumbLabels name a thumb. The visible label is linked once JavaScript runs.
  • A range's thumbs read as 'start range' and 'end range' by default. Name them with thumbLabels.
  • The focus ring sits on the thumb while its input has keyboard focus. It uses --ring, which the theme tests hold to 3:1.
  • The whole row, as tall as a button, takes clicks and touches, not just the thin track.
  • The fill uses the tone's color on the muted track. No contrast pair checks the fill or the track, so the label and value carry the meaning.

Examples

With a label and value

<Slider label="Volume" defaultValue={60} showValue tone="brand" />

Price range

<Slider
  label="Price"
  defaultValue={[20, 80]}
  max={200}
  step={5}
  minStepsBetweenValues={2}
  showValue
  format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
  thumbLabels={["Lowest price", "Highest price"]}
/>

No visible label

<Slider aria-label="Brightness" defaultValue={40} size="sm" className="max-w-48" />

Save when the drag ends

"use client";
import { useState } from "react";

function TextSize({ save }: { save: (size: number) => void }) {
  const [size, setSize] = useState(16);
  return (
    <Slider
      label="Text size"
      showValue
      min={12}
      max={24}
      value={size}
      onValueChange={(value) => setSize(value as number)}
      onValueCommitted={(value) => save(value as number)}
    />
  );
}