Slider
betaA 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
size
range
states
vertical
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| label | ReactNode | - | Visible text above the track. Without it, TypeScript requires aria-label or aria-labelledby. |
| aria-label | string | - | Names the slider when there is no visible label. It goes on each thumb. |
| aria-labelledby | string | - | The id of visible text elsewhere that names the slider. It goes on each thumb. |
| showValue | boolean | false | Shows the value as text at the end of the label row. A range shows both ends, like '20 – 80'. |
| thumbLabels | readonly string[] | - | A name for each thumb of a range slider, in order. It replaces the label as each thumb's name. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | Color of the filled part and the thumb's ring. |
| size | "sm" | "md" | "lg" | "md" | Track and thumb size, and label text size. The row is as tall as a button of the same size. |
| value | number | number[] | - | Controlled value. An array makes a range, with one thumb per value. Use it with onValueChange. |
| defaultValue | number | number[] | - | Starting value when you do not control it. An array makes a range. |
| onValueChange | (value: number | number[], details) => void | - | Called on every move, while dragging too. |
| onValueCommitted | (value: number | number[], details) => void | - | Called once when a drag or key press ends. Save here, not in onValueChange. |
| min | number | 0 | The lowest value. |
| max | number | 100 | The highest value. |
| step | number | 1 | How far one arrow key press moves. Values snap to it. |
| largeStep | number | 10 | How far Page Up, Page Down, or Shift with an arrow moves. |
| minStepsBetweenValues | number | 0 | For a range, the fewest steps the two thumbs can be apart. |
| format | Intl.NumberFormatOptions | - | How the value reads, as text and to screen readers, like { style: 'currency', currency: 'USD' }. |
| orientation | "horizontal" | "vertical" | "horizontal" | Vertical sliders are 10rem tall. className='h-64' changes it. |
| disabled | boolean | false | Blocks moving it and removes it from the tab order. It dims. |
| name | string | - | Form field name. The value is sent with the form. |
| className | string | - | Classes for the root. Later classes win, so className='max-w-xs' narrows it. |
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)}
/>
);
}