Range Slider

Two thumbs over one track. Slider is a restyled native input[type=range], which has exactly one thumb by construction — so a range needs a custom control with the ARIA written out.

src/components/ui/range-slider.tsx

About

From the component's own source, where it was written.

Two thumbs over one track, for a minimum and maximum.

Slider is a restyled native input[type=range], which has exactly one thumb by construction — a range cannot be built from it, so this is a custom control with the ARIA and keyboard behaviour written out.

Track and thumb come from the shared sliderSize scale, which sets --slider-track and --slider-thumb — so a range slider and a plain one of the same size line up exactly.

Both thumbs are real buttons with role="slider", so each is a tab stop with its own value and its own bounds. A single element reporting two numbers has no way to be announced or operated.

The thumbs can cross: dragging the low one past the high one swaps which is which rather than stopping dead at the other. Clamping is the more common implementation and it feels broken — the pointer keeps moving while the thumb stays put.

Install

npx astralyx-ui add range-slider

Usage

import { RangeSlider } from '@/components/ui/range-slider'

<RangeSlider min={0} max={1000} step={10} value={range} onValueChange={setRange} />

Composer

Change the props and the source below follows.

Price$200 – $800
Props
tsx
<RangeSlider
  min={0}
  max={1000}
  step={10}
  value={range}
  onValueChange={setRange}
  showValues
/>

Examples

Price range

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

RangeSlider

Extends Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange'>.

PropTypeDefaultDescription
minnumber0—
maxnumber100—
stepnumber1—
valueRangeValue—Always sorted low-to-high on write.
defaultValueRangeValue[25, 75]—
onValueChange(value: RangeValue) => void—Always sorted low-to-high on write.
onValueCommit(value: RangeValue) => void—Fires once on release, for anything expensive to run per frame.
disabledbooleanfalse—
sizekeyof typeof sliderSize'default'Reads the shared sliderSize tokens, so it lines up with a plain Slider of the same size.
labelstring——
formatValue(value: number) => stringDEFAULT_FORMAT_VALUE—
showValuesbooleanfalse—

Types

RangeValue

Two thumbs over one track, for a minimum and maximum.

FieldTypeDescription
0number—
1number—
length2—

Behaviour

TopicDetailDescription
crossingthumbs swapDragging the low thumb past the high one swaps which is which. Clamping is more common and feels broken — the pointer keeps moving while the thumb stays put.
accessibilitytwo slidersEach thumb is a button with role="slider" and its own bounds, so both are tab stops and both announce.