Starter Docs
UI

Slider

Native range inputs as a slider or a range. The thumb grows on a spring when pressed and a bubble follows it with the value. Every animation is CSS.

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/slider

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/slider.json

The files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

Usage

Installed from the registry, import from @/components/ui/slider; inside this monorepo, from @rabtx/ui/slider.

import { Slider } from "@rabtx/ui/slider";

export function PriceFilter() {
  const [price, setPrice] = useState([200, 800]);
  return (
    <Slider aria-label="Price" value={price} onValueChange={setPrice} max={1000} step={10} />
  );
}

Values are arrays, as in shadcn: one number is a slider, two (or more) a range. Every thumb is an <input type="range">, so the browser provides the slider role, the value, minimum and maximum for screen readers, arrow keys, Page Up and Down, Home and End, and the form value. Name it with a Field or an aria-label.

Value bubble

showValue puts the value over the thumb while it is hovered, focused or dragged; "always" keeps it out. formatValue formats the bubble and the value read to screen readers (aria-valuetext). The bubble is decorative (aria-hidden): the input already announces the value.

Marks

marks draws ticks under the track (never under the thumb): true for every step (up to 100), or a list of values, each with an optional label. Ticks inside the filled part take the fill color. The values also fill a <datalist> linked with the inputs' list attribute.

Ruler

variant="ruler" swaps the round thumb for a needle over a tick scale, with a large readout above. The scale draws a tick every step (or every few steps, so there are at most 100); labelled marks add taller ticks underneath.

Vertical

orientation="vertical" stands the slider up with the native vertical writing mode, minimum at the bottom; Arrow Up raises it. Give it a height (12rem by default). Bubbles open to the side.

States

Disabled is dimmed to 50%, skipped by Tab and ignores the pointer. Inside a Field the label names the slider, the hint describes it, and error turns the rail edge and the thumb --destructive and links the message.

Managed by your admin.

Pick a budget of at least 20.

Range and controlled

Two values make a range: one input per thumb, overlaid on a shared track. Each thumb is clamped between its neighbours, minStepsBetweenThumbs apart, so they can meet but never cross. Pressing the bare track moves the nearest thumb there and follows the pointer. onValueChange fires on every move; onValueCommit fires when a drag or a key press ends (the native change event).

$200 to $800 (last committed $200 to $800)

Forms

A slider submits under its name; a range submits one entry per thumb, read with formData.getAll(name).

Arrow keys move it by 1, Page Up and Down by 10.

Not sent yet

Right to left

The native range runs from the inline start, and the fill, bubbles and marks follow it.

API

Slider takes everything a <div> takes (on the root), plus:

Prop

Type

Outside React, the class names are exported from @rabtx/ui/slider/styles. The fill reads --_lo and --_hi (numbers from 0 to 1) from the root:

<div class="rx-slider rx-slider-md" data-orientation="horizontal" style="--_lo: 0; --_hi: 0.4">
  <div class="rx-slider-control">
    <div class="rx-slider-layer">
      <input type="range" class="rx-slider-input" value="40" aria-label="Volume" />
    </div>
  </div>
</div>

Theming

The Slider reads only the theme tokens.

PartTokens
Rail and fillrail --muted-foreground (22% over --background), fill --primary
Thumb--background with a --primary edge; halo --primary at 18%
Bubble--foreground with --background text
Marks--muted-foreground, inside the fill --primary
Depth (Floating)rail --field-edge-top, --field-edge-bottom; thumb --surface-ring, --surface-sheen, --surface-shade
Focus and error--ring, --destructive
Corners and motion--radius-button, --radius-item (bubble), --spring-press, --spring-swap, --motion-ease

How it works

EffectCSS
Fillthe component writes --_lo and --_hi; the rail is one linear-gradient with stops at thumb / 2 + (100% - thumb) * p, the path the native thumb's centre travels, on ::-webkit-slider-runnable-track and ::-moz-range-track (a range paints it once, on a shared track under the inputs)
Rangeone input per thumb, stacked; the inputs ignore the pointer and their thumbs take it back (pointer-events: auto), so each thumb drags natively; the last-used thumb sits on top, and thumbs meeting at the maximum put the lower one on top
Press:active grows the thumb to 1.2 with a 5px halo on the press spring (500 / 30 / 0.6); hover shows a 4px halo
Bubblepositioned at --_p with the same formula, so it stays on the thumb; out on :hover, :focus-visible and :active with scale 0.5 to 1 and 6px up on the swap spring, away in 120ms
Rulerthe scale is the fill gradient masked by a repeating-linear-gradient of 1px ticks, intersected with a lower-half mask
Verticalwriting-mode: vertical-lr; direction: rtl on the input, so the browser lays the range out bottom to top

JavaScript only keeps the values, clamps the thumbs and handles a press on a range's bare track. Thumbs cannot be anchors for CSS anchor positioning (they are pseudo-elements), which is why the bubble reads a variable instead.

Not built, because they need JavaScript animation: a bubble that leans with drag velocity, a ruler scale that scrolls under a fixed needle with momentum, bars that rise into a travelling crest, and a thumb that parts around inline labels. The thumb itself jumps with the native input rather than gliding on a spring, so the value under the pointer is always exact.

Browser support

  • Everywhere: appearance: none on ranges, the WebKit and Firefox thumb and track pseudo-elements, color-mix(), nesting, :has(), mask-composite and linear() easings: Chrome 120+, Safari 17.2+ and Firefox 128+ (from compatibility data; tested in Chrome).
  • Vertical: writing-mode on a range needs Chrome 124+, Safari 17.4+ or Firefox 120+.

Accessibility

  • Every thumb is a native slider: role, value, minimum, maximum and orientation come from the input, and formatValue adds aria-valuetext.
  • A range's thumbs are named by the label and their own name ("Price Minimum", "Price Maximum"), with aria-labelledby pointing at the Field label and the thumb.
  • Arrow keys step, Page Up and Down move by a tenth, Home and End go to the ends; a range thumb stops at its neighbour.
  • Focus shows a 2px --ring outline around the thumb. Value is shown by position and the fill, never motion alone; reduced motion only fades the bubble and data-motion="off" moves nothing.

On this page