Base components
Slider
A track with one or two handles for picking a value or a range, where the handle and the fill follow the pointer with no delay and the value label rides along with the handle.
Live preview
Loading preview
A Volume slider with a floating label, a Price range slider from $20 to $80 with labels under the handles and a 10 step gap, and a disabled slider. Replay moves the volume from 20 to 70 with key steps, then the range ends inward.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add sliderUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { Slider } from './slider';
import type { SliderValue } from './slider-variants';
const VOLUME_STEPS = [20, 30, 40, 50, 60, 70];
const money = (value: number) => `$${value}`;
const percent = (value: number) => `${value}%`;
function Field({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-secondary">{title}</p>
{children}
</div>
);
}
export default function SliderDemo({ replay = 0 }: { replay?: number }) {
const [volume, setVolume] = React.useState<SliderValue>(40);
const [price, setPrice] = React.useState<SliderValue>([20, 80]);
React.useEffect(() => {
if (replay === 0) return;
const timers: number[] = [];
const at = (ms: number, run: () => void) => timers.push(window.setTimeout(run, ms));
setVolume(VOLUME_STEPS[0] ?? 20);
setPrice([20, 80]);
VOLUME_STEPS.slice(1).forEach((value, index) => {
at(400 + index * 180, () => setVolume(value));
});
at(1500, () => setPrice([30, 70]));
return () => {
for (const timer of timers) window.clearTimeout(timer);
};
}, [replay]);
return (
<div className="flex w-full max-w-sm flex-col gap-8">
<Field title="Volume">
<Slider
aria-label="Volume"
label="top-floating"
format={percent}
value={volume}
onValueChange={setVolume}
/>
</Field>
<Field title="Price">
<Slider
aria-label="Price"
label="bottom"
format={money}
minStepsBetweenThumbs={10}
value={price}
onValueChange={setPrice}
/>
</Field>
<Field title="Brightness">
<Slider aria-label="Brightness" defaultValue={60} format={percent} disabled />
</Field>
</div>
);
}
API
Parts: root, track, fill (from the start, or between the two handles), handle (one or two), value label (under the handle, or a floating label above it).
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | [number, number] | undefined | Controlled value. A pair makes a range slider with two handles. |
| defaultValue | number | [number, number] | 0 | Value when uncontrolled. |
| min | number | 0 | Lowest value. |
| max | number | 100 | Highest value. |
| step | number | 1 | Step for the arrow keys and for dragging. |
| largeStep | number | 10 | Step for Page Up, Page Down and Shift with an arrow key. |
| minStepsBetweenThumbs | number | 0 | Range only: how close the two handles may come. |
| label | "none" | "bottom" | "top-floating" | "none" | Value label under each handle, in a floating label above it, or none. |
| format | (value: number) => string | the number, for example "40" | Text of the value label and of aria-valuetext, for example "$40" or "40%". |
| disabled | boolean | false | Fades the slider to 50% and turns it off. |
| name | string | undefined | Form field name. |
| Event | Description |
|---|---|
| valueChange | The value changed while dragging or with the keys. Payload: the new value. |
| valueCommit | The pointer was released or a key step ended. Payload: the final value. Use it for slow work such as requests. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| SLD1 | drag a handle | handle, fill, value label | translate x (handle and label), width (fill) | pointer start → pointer position, snapped to step | 0ease-linear | Moves at once |
| SLD2 | press the track away from a handle | nearest handle, fill, value label | translate x, width | old value → value at the pointer | duration-md (260)ease-out | Moves at once |
| SLD3 | arrow keys, Page Up, Page Down, Home or End | focused handle, fill, value label | translate x, width | old value → new value | duration-2xs (120)ease-out | Moves at once |
| SLD4 | hover or drag a handle | handle | background-color, box-shadow | slider-handle-bg, shadow-md → bg-brand-secondary, shadow-lg | duration-sm (200)ease-out | Swaps at once |
| SLD5 | keyboard focus on a handle | handle | box-shadow | shadow-md → focus ring (2px bg-primary gap, 4px focus-ring) | duration-2xs (120)ease-out | Shows at once |
| SLD6 | the value changes (any way) | value label text | text | old value → new value, at once (no roll while dragging) | 0ease-linear | Swaps at once |
Accessibility
- Built on a slider primitive: each handle is role slider with aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext from format.
- Name each handle: one label for a single slider, and "Minimum price" and "Maximum price" style names for a range.
- Keyboard: Left and Down step down, Right and Up step up, Page Up and Page Down (or Shift with an arrow) use largeStep, Home and End jump to min and max.
- The handle is 24px, which meets the 24 x 24px target (WCAG 2.5.8); the track takes presses along its full height plus 8px above and below.
- Value labels are aria-hidden; screen readers read aria-valuetext.
- Reduced motion: dragging still moves the handle (it is direct), and track presses and key steps jump with no tween.