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
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/sliderOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/slider.jsonThe 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).
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.
| Part | Tokens |
|---|---|
| Rail and fill | rail --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
| Effect | CSS |
|---|---|
| Fill | the 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) |
| Range | one 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 |
| Bubble | positioned 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 |
| Ruler | the scale is the fill gradient masked by a repeating-linear-gradient of 1px ticks, intersected with a lower-half mask |
| Vertical | writing-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: noneon ranges, the WebKit and Firefox thumb and track pseudo-elements,color-mix(), nesting,:has(),mask-compositeandlinear()easings: Chrome 120+, Safari 17.2+ and Firefox 128+ (from compatibility data; tested in Chrome). - Vertical:
writing-modeon 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
formatValueaddsaria-valuetext. - A range's thumbs are named by the label and their own name ("Price Minimum", "Price Maximum"),
with
aria-labelledbypointing 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
--ringoutline around the thumb. Value is shown by position and the fill, never motion alone; reduced motion only fades the bubble anddata-motion="off"moves nothing.
Radio Group
Native radios in a radiogroup fieldset, three sizes and choice cards. The dot glides to the chosen option, all in CSS.
Tabs
WAI-ARIA tabs in segmented, pill, underline and expandable variants. The indicator slides to the selected tab by CSS anchor positioning on a spring, and panels fade in. Every animation is CSS.