Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Slider

Pick a number or a range from a continuous track — keyboard accessible, with optional steps. Built on Base UI Slider.

Status
stable
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/slider

Usage

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, and disabled state. Exposes data-slot="slider".
  • Control — the interactive hit area; pressing it jumps the nearest thumb.
  • Track — the rail spanning the full minmax range (surface-1 on the default variant; see Variants).
  • Indicator — the filled portion from min (or the lower thumb) up to the value (primary on the default variant; 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.).

60

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.

variantWhere
defaultThe form rail — surface-1 track, primary fill, hollow ringed thumb.
mediaA transport seek on a normal surface: muted fill that brightens on engagement.
overlayChrome drawn over video, on the theme-invariant --media-* ink.
bareA 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

PropTypeDefaultDescription
defaultValuenumber | readonly number[]Initial value for an uncontrolled slider. Use an array for a range.
disabledbooleanfalseIgnore 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.
marksboolean | readonly number[]falseDecorative tick marks (aria-hidden). true derives one tick per step between min and max; an array places ticks at exactly those values.
maxnumber100Highest selectable value.
minnumber0Lowest 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.
renderComponentRenderFn<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 (ControlTrackIndicator + Thumb) as children.
showValuebooleanfalseShow 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.
stepnumber1Granularity the value snaps to when stepping. Decimals are supported.
thumbSliderThumbVisibility'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.
thumbAriaLabelsreadonly 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).
valuenumber | 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.
variantSliderVariant'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

AttributeValues
data-slot"slider" | "slider-control" | "slider-indicator" | "slider-mark" | "slider-marks" | "slider-thumb" | "slider-track" | "slider-value"
data-thumbmirrors a prop or state value
data-variantmirrors a prop or state value
--slider-markCSS custom property

Accessibility

  • Each thumb renders a native <input type="range"> with role="slider" and aria-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; pass thumbAriaLabels (for example ['Minimum price', 'Maximum price']) or getThumbAriaLabel.
  • :focus-visible shows a 2px ring (outline-ring) on the focused thumb — never outline: none.
  • disabled removes the thumb(s) from the tab order and blocks all interaction.
KeyAction
TabMove focus to the next thumb
/ Decrease the value by one step
/ Increase the value by one step
Page Down / Page UpDecrease / increase by the large step
HomeJump to the minimum
EndJump to the maximum
ContractStates tested
Behaviourdefault, disabled, dragging, range, vertical
Accessibilitydisabled, focus-visible, labeled, semantic-html
Visualdefault, disabled, marks, value-readout

Do / Don't

Do
Use a Slider for an imprecise value over a known range — volume, brightness, a price filter — and give it an aria-label.
Don't
Use a Slider when the exact value matters or the range is large and open-ended — use a number input instead.

On this page