A range picked on a ruler: each thumb is a lens that spreads and lifts the ticks around it, the two value bubbles run into one like drops when the thumbs meet, and marks hold a thumb that comes near.
Installation
When to use
- A range people judge against something: a pay band against the market, a price filter against what's for sale, a budget against past spend. Pass that as `distribution` and the ticks take its shape.
- A scale with points worth stopping at (a median, a limit, a plan's quota): `marks` pull the thumb onto them.
When not to use
- One value, or a range in a form among other fields: the kit's Slider is smaller and a Field.
- Exact numbers people already know: two Number Inputs type them faster.
Usage
Range Slider with your own data: a whole component you can paste as a page.
"use client"
import { RangeSlider } from "@/components/dashral/pro/range-slider"
import { useState } from "react"
export default function PriceFilter() {
const [price, setPrice] = useState<[number, number]>([80, 240])
return (
<div className="w-full max-w-sm">
<RangeSlider
label="Price per night"
min={0}
max={500}
step={5}
value={price}
onValueChange={setPrice}
// Once the drag ends: the place to load results from.
onValueCommit={(range) => console.log("load stays from", range[0], "to", range[1])}
marks={[{ value: 120, label: "Typical" }, { value: 350, label: "Luxury" }]}
formatOptions={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
/>
</div>
)
}API reference
RangeSlider
A range slider on a ruler of ticks: the ticks swell around each thumb and darken inside the range, the two values ride in bubbles that run into one when the thumbs come close, and marks pull a thumb onto them. Controlled or not.
| Prop | Type | Default |
|---|---|---|
value | [number, number]The range, low then high. Leave it out (with | — |
defaultValue | [number, number] | — |
onValueChange | (value: [number, number]) => voidEach change while dragging or by key. | — |
onValueCommit | (value: [number, number]) => voidOnce a drag ends or a key is let go: the place to load from. | — |
min | number | 0 |
max | number | 100 |
step | number | 1 |
minGap | numberThe least the two thumbs stay apart, in the value's units; one | — |
marks | RangeSliderMark[]Points worth stopping at (a median, a limit): a dot on the track and, with a label, a word under it. | — |
snap | numberHow far a mark reaches, in px: inside it a dragged thumb lingers by the mark and the value is the mark's exactly. 0 turns it off. | 14 |
distribution | number[]How much there is along the scale (a histogram, from | — |
ticks | numberHow many ticks; by default as many as fit, about one every 9px. | — |
bubbles | "always" | "active" | falseThe values over the thumbs: always, only while one is dragged or focused, or never. | "always" |
formatOptions | Intl.NumberFormatOptionsHow a value is written, e.g. | — |
formatValue | (value: number) => stringYour own words for a value, instead of | — |
color | "neutral" | "primary"The range and the ticks inside it: ink, or the theme's primary. | "neutral" |
disabled | boolean | — |
label | stringThe slider's name, for screen readers. | "Range" |
thumbLabels | [string, string]Each thumb's name. | ["Minimum", "Maximum"] |
className | string | — |
classNames | RangeSliderClassNames | — |
type RangeSliderMark
| Prop | Type | Default |
|---|---|---|
value | number | — |
label | stringWritten under the track; a click on it brings the nearest thumb. Where two labels would touch, the one later in | — |
type RangeSliderClassNames
| Prop | Type | Default |
|---|---|---|
root | string | — |
bubble | string | — |
tick | string | — |
track | string | — |
range | string | — |
thumb | string | — |
mark | string | — |
markLabel | string | — |
Keyboard
| Keys | Action |
|---|---|
| Tab | Reach the low thumb, then the high one |
| ←or→ | One step down or up (mirrored right to left); ↓ and ↑ do the same |
| Shift + ←or→ | Ten steps |
| Page UporPage Down | The next or previous mark; with no marks, ten steps |
| HomeorEnd | As far as it goes: the scale's end, or the other thumb less the gap |
Accessibility
- A group with your `label`, holding two sliders named by `thumbLabels`; each says its value as you write it (`formatValue` or `formatOptions`), with a mark's label when it sits on one.
- A thumb's limits follow the other thumb, so a screen reader hears how far it can go; the two never cross.
- The bubbles, ticks and mark labels are hidden from screen readers: they repeat what the sliders say. Thumbs have a 42px target and show a ring on keyboard focus.
Motion
- Each thumb is a lens on the ruler: the ticks near it are pushed apart and stand taller, those further out gather, and none change places. The lens is quiet at rest, stronger under the pointer or on focus, and strongest while the thumb is dragged (0.3 s between them).
- A drag is 1:1. Each bubble trails its thumb by a few px and catches up without overshooting, its arrow staying on the thumb. A press on the ruler sets the value at once while the thumb glides there (0.22 s); keys, a mark's label and a new value glide too.
- As the bubbles meet they slide together and join like two drops, the dash fading in between the values (0.42 s), and part the same way. The values slide to their places, never swapped for a copy.
- Inside a mark's reach (`snap`, 14px) the value is the mark's and the thumb lingers by it, meeting the pointer again at the edge; let go there and it settles onto the mark. Catching a mark beats its label once, and phones buzz lightly.
- Smooth tweens on [0.4, 0, 0.2, 1], never springs. With reduced motion everything is set at once: no trailing, no beat, and marks still take their values.