Slider
Pick a number or a range from a continuous track — keyboard accessible, with optional steps. Built on Base UI Slider.
- Status
- Since
0.1.0- Accessibility pattern
- APG slider
Last updated
Install
Add Slider from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/sliderUsage
import { Slider } from "@/components/ui/slider";
<Slider defaultValue={40} aria-label="Volume" />;Pass an array for a range (one thumb per value):
<Slider
defaultValue={[20, 80]}
thumbAriaLabels={["Minimum price", "Maximum price"]}
/>Examples
Anatomy
Slider composes Base UI's Slider parts internally — you use the single Slider
export and it renders the structure for you:
<Slider.Root>
{/* the clickable hit area */}
<Slider.Control>
{/* the rail (surface-1 by default; per-variant) */}
<Slider.Track>
{/* the filled portion (primary by default; per-variant) */}
<Slider.Indicator />
{/* one draggable handle per value; each owns a hidden <input type="range"> */}
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>- Root — owns the value,
min/max/step, anddisabledstate. Exposesdata-slot="slider". - Control — the interactive hit area; pressing it jumps the nearest thumb.
- Track — the rail spanning the full
min→maxrange (surface-1on thedefaultvariant; see Variants). - Indicator — the filled portion from
min(or the lower thumb) up to the value (primaryon thedefaultvariant; see Variants). - Thumb — the draggable handle. One is rendered per value, so a single number gets
one thumb and an array gets a range. Each thumb wraps a hidden
<input type="range">for keyboard control and form submission.
Range
Provide an array to render a range. The component renders one thumb per entry and keeps the thumbs from crossing.
Steps
Set min, max, and step to snap the value to a coarser scale — here, multiples of
50 between 0 and 1000.
Controlled
Pass value + onValueChange to drive the slider from your own state (mirror it back as
text, sync it to a query param, etc.).
Thumb labels
A range needs a distinct accessible name per thumb. Pass a static thumbAriaLabels
array (see Range), or compute names dynamically with getThumbAriaLabel — a
callback that receives the thumb's index and current value and returns its name.
getThumbAriaLabel takes precedence over the slider-level aria-label fallback, so it's
the right tool when the name should reflect live state (for example announcing the value).
<Slider
defaultValue={[20, 80]}
getThumbAriaLabel={(index, value) =>
`${index === 0 ? "Minimum" : "Maximum"} price, $${value ?? 0}`
}
/>Orientation
orientation="vertical" runs the rail bottom-to-top; every part keys off data-orientation,
so the track, fill, thumb and tick marks all follow. Give the wrapper a height — a vertical
rail fills its container the way a horizontal one fills its width. The media players' volume
control is the canonical use.
Marks
marks draws decorative tick marks on the rail: true derives one per step, or pass an array
to place them at exact values. They are aria-hidden — the thumb's native range input already
carries min/max/step for assistive tech, so ticks would only duplicate it.
Value readout
showValue floats the formatted value above the active thumb, on drag and on keyboard focus
only — a range gets one label per thumb. It is not a replacement for a visible label; it is the
during-the-gesture feedback that tells you where you landed.
Variants
variant picks the rail, fill and thumb recipe, and thumb decides when the thumb is drawn.
variant | Where |
|---|---|
default | The form rail — surface-1 track, primary fill, hollow ringed thumb. |
media | A transport seek on a normal surface: muted fill that brightens on engagement. |
overlay | Chrome drawn over video, on the theme-invariant --media-* ink. |
bare | A transparent hit layer over custom-drawn media (the audio waveform), with no drawn rail. |
thumb="always" (the default) draws the thumb at rest. thumb="hover" hides it at rest only
where a pointer can hover and reveals it on hover, focus and drag; on a touch device it stays
visible, because there is no hover to reveal it with. thumb="none" draws no thumb at all — for
a rail where the fill itself is the position cue.
Disabled
disabled drops the thumb(s) from the tab order, blocks interaction, and dims the control.
It applies to both single-value and range sliders — every thumb is dimmed and removed from
the tab order.
A disabled range dims both thumbs:
Playground
Every Slider prop, auto-generated from its TypeScript types.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | readonly number[] | — | Initial value for an uncontrolled slider. Use an array for a range. |
disabled | boolean | false | Ignore user interaction, drop the thumb(s) from the tab order, and dim the control. |
getThumbAriaLabel | ((index: number, value: number | undefined) => string | undefined) | — | Builds an accessible name for a thumb from its index and current/default
value. Takes precedence over the slider-level aria-label fallback. |
marks | boolean | readonly number[] | false | Decorative tick marks (aria-hidden). true derives one tick per step
between min and max; an array places ticks at exactly those values. |
max | number | 100 | Highest selectable value. |
min | number | 0 | Lowest selectable value (the origin for step). |
onValueChange | ((value: number | readonly number[], eventDetails: BaseSlider.Root.ChangeEventDetails) => void) | — | Called with the new value (and event details) on every change while dragging or stepping via the keyboard. |
orientation | "horizontal" | "vertical" | 'horizontal' | Track direction. vertical lays the rail out bottom-to-top and swaps the
arrow keys accordingly — used by the media volume control. Base UI writes
data-orientation onto every part, and each part's own layout keys off it. |
render | ComponentRenderFn<HTMLProps, SliderRootState> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Replace the rendered root element via Base UI render composition. Pass a
ReactElement or a render function — Base UI merges this
wrapper's className, data-slot, and state data-* onto your element,
forwards the ref, and keeps the slider internals (Control → Track →
Indicator + Thumb) as children. |
showValue | boolean | false | Show the formatted value in a small floating label above the thumb, on drag and keyboard focus only (motion register M-05). Range sliders get one label per thumb. |
step | number | 1 | Granularity the value snaps to when stepping. Decimals are supported. |
thumb | SliderThumbVisibility | 'always' | Thumb visibility. hover hides the thumb at rest on hover-capable devices
only and reveals it on hover, focus or drag — on a touch device (hover:
none) it stays visible, because there is no hover to reveal it with (audit
B4-04). none draws no thumb at all (the bare waveform layer), while
keeping it focusable so arrow/Home/End still work. |
thumbAlignment | "center" | "edge" | "edge-client-only" | 'center' | Where a thumb sits relative to the track ends. center lets the thumb
overhang the rail by half its width at either extreme; edge insets it so
it stays inside the rail — what a short media volume rail wants. |
thumbAriaLabels | readonly string[] | — | Accessible names for each thumb. Required for a range when the default
generated labels are not specific enough (for example Minimum price /
Maximum price). |
value | number | readonly number[] | — | Slider value. A single number renders one thumb; an array (e.g. [20, 80])
renders a range with one thumb per entry. Controlled — pair with onValueChange. |
variant | SliderVariant | 'default' | Visual treatment. media is the audio-card transport, overlay is chrome
drawn over video (theme-invariant --media-* ink), and bare is an
invisible hit/keyboard layer over custom-drawn media such as a waveform. |
Data attributes and CSS variables on Slider
| Attribute | Values |
|---|---|
data-slot | "slider" | "slider-control" | "slider-indicator" | "slider-mark" | "slider-marks" | "slider-thumb" | "slider-track" | "slider-value" |
data-thumb | mirrors a prop or state value |
data-variant | mirrors a prop or state value |
--slider-mark | CSS custom property |
Accessibility
- Each thumb renders a native
<input type="range">withrole="slider"andaria-valuenow/aria-valuemin/aria-valuemax, so screen readers announce the current value and bounds. - Fully keyboard operable — focus a thumb and use the arrow keys (see the table below). Range thumbs are independently focusable.
- Always give the slider an accessible name via
aria-label(or wire it to a<label>/Field). A range needs a distinct name for each thumb; passthumbAriaLabels(for example['Minimum price', 'Maximum price']) orgetThumbAriaLabel. :focus-visibleshows a 2px ring (outline-ring) on the focused thumb — neveroutline: none.disabledremoves the thumb(s) from the tab order and blocks all interaction.
| Key | Action |
|---|---|
| Tab | Move focus to the next thumb |
| ← / ↓ | Decrease the value by one step |
| → / ↑ | Increase the value by one step |
| Page Down / Page Up | Decrease / increase by the large step |
| Home | Jump to the minimum |
| End | Jump to the maximum |
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, dragging, range, vertical |
| Accessibility | disabled, focus-visible, labeled, semantic-html |
| Visual | default, disabled, marks, value-readout |