Early bird: 30% off Dashral ProSee pricing

Dashral

Range SliderPro

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.

example.tsx
"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.

PropTypeDefault
value[number, number]

The range, low then high. Leave it out (with defaultValue) to let the slider keep it.

—
defaultValue[number, number]—
onValueChange(value: [number, number]) => void

Each change while dragging or by key.

—
onValueCommit(value: [number, number]) => void

Once a drag ends or a key is let go: the place to load from.

—
minnumber0
maxnumber100
stepnumber1
minGapnumber

The least the two thumbs stay apart, in the value's units; one step by default.

—
marksRangeSliderMark[]

Points worth stopping at (a median, a limit): a dot on the track and, with a label, a word under it.

—
snapnumber

How 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
distributionnumber[]

How much there is along the scale (a histogram, from min to max): the ticks take its shape. Flat without it.

—
ticksnumber

How many ticks; by default as many as fit, about one every 9px.

—
bubbles"always" | "active" | false

The values over the thumbs: always, only while one is dragged or focused, or never.

"always"
formatOptionsIntl.NumberFormatOptions

How a value is written, e.g. { style: "currency", currency: "USD" }.

—
formatValue(value: number) => string

Your own words for a value, instead of formatOptions.

—
color"neutral" | "primary"

The range and the ticks inside it: ink, or the theme's primary.

"neutral"
disabledboolean—
labelstring

The slider's name, for screen readers.

"Range"
thumbLabels[string, string]

Each thumb's name.

["Minimum", "Maximum"]
classNamestring—
classNamesRangeSliderClassNames—

type RangeSliderMark

PropTypeDefault
valuenumber—
labelstring

Written under the track; a click on it brings the nearest thumb. Where two labels would touch, the one later in marks is left out.

—

type RangeSliderClassNames

PropTypeDefault
rootstring—
bubblestring—
tickstring—
trackstring—
rangestring—
thumbstring—
markstring—
markLabelstring—

Keyboard

KeysAction
TabReach 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 DownThe next or previous mark; with no marks, ten steps
HomeorEndAs 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.