Headless / Numeric

RangeSlider

RangeSlider captures a low–high pair on a single scale. Two thumbs, each a role="slider", that clamp against each other so the low value can never pass the high.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A single component holding a [number, number] value. It renders two thumbs; each is its own role="slider" with its own aria-valuenow. The thumbs constrain one another — the low thumb's aria-valuemax is the high value, and the high thumb's aria-valuemin is the low value — so a drag or a keypress can narrow the range but never invert it.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { RangeSlider } from '@usemotif/headless';

API

RangeSlider

stable
function RangeSlider(props: RangeSliderProps): JSX.Element
value / defaultValue / onValueChange[number, number] / [number, number] / (next) => void

The low–high pair, controlled or uncontrolled.

min / max / stepnumber= 0 / 100 / 1

The scale bounds and the increment.

disabledboolean= false

Removes both thumbs from the Tab order and ignores input.

aria-label / aria-labelledbystring

Names the range control.

styleCSSProperties

Inline style for the track wrapper.

Keyboard

Each thumb is focusable on its own. With a thumb focused, Arrow Right and Up increase it, Arrow Left and Down decrease it, and Home and End move it to its current bound — for the low thumb, min and the high value; for the high thumb, the low value and max.

Accessibility

Each thumb is a separate role="slider" with its own value and its own announced range, so a screen-reader user moves between the two and adjusts each independently. The crossing constraint lives in the ARIA bounds, not just the visuals — the low thumb genuinely cannot be keyed past the high one.

Examples

A price-range filter:

example.tsx
<RangeSlider
aria-label="Price range"
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={500}
step={10}
/>