RangeSlider
A dual-thumb slider that selects a min to max interval on one track, with paired number fields for exact entry. The thumbs can meet but never cross.
Import
import { RangeSlider } from "@hyzer-labs/ui"Demo
Two thumbs, one track, and the fill spans the selected interval. Drag a thumb past its partner and it clamps: the pair can meet but never cross. Typing in a number field clamps the same way, against the partner's value.
let lengthMin = $state(200);
let lengthMax = $state(450);
<RangeSlider
name="length"
label="Hole length"
min={100}
max={900}
unit="ft"
bind:valueMin={lengthMin}
bind:valueMax={lengthMax}
/>Same ticks API as Slider: visual marks only, with no snapping.
<RangeSlider
name="length"
label="Hole length"
min={100}
max={900}
unit="ft"
ticks={[
{ value: 300, label: 'par 3' },
{ value: 550, label: 'par 4' },
{ value: 800, label: 'par 5' }
]}
/>showInput={false} swaps the field pair for a single read-only min–max readout.
<RangeSlider name="rating" label="Rating band" min={700} max={1100} step={5} valueMin={850} valueMax={975} showInput={false} />orientation="vertical" switches both ranges to a native writing-mode: vertical-lr track. It grows bottom-up, so Up and Right arrows
still increase toward max on either thumb. The track's block length comes
from --hz-slider-length (default 12rem), so it never grows the
page unbounded.
The two exact-entry fields stay one inline cluster, separated by a dash. They sit above
or below the track (per inputPosition) rather than stacking to mirror the
thumbs. This demo wraps two sliders side by side in Cluster.
<Cluster gap="lg">
<RangeSlider
name="hole-length-vertical"
label="Hole length (vertical)"
min={100}
max={900}
unit="ft"
orientation="vertical"
ticks={[
{ value: 300, label: 'par 3' },
{ value: 550, label: 'par 4' },
{ value: 800, label: 'par 5' }
]}
/>
<RangeSlider
name="hole-length-vertical-start"
label="Hole length (vertical, input above)"
min={100}
max={900}
unit="ft"
orientation="vertical"
inputPosition="start"
/>
</Cluster>description and error are announced with each thumb: both
chain into aria-describedby on both ranges. Error and disabled are also
field states. The fieldset's data-state reflects them, and error wins.
<RangeSlider
name="weight"
label="Disc weight range"
min={150}
max={180}
unit="g"
description="Most players bag 165–175 g drivers."
/>
<RangeSlider name="par" label="Par range" max={6} error="Pick a par range to filter holes." />
<RangeSlider name="entry" label="Entry fee split" required />
<RangeSlider name="locked" label="Course length filter (locked)" min={3000} max={12000} valueMin={5000} valueMax={9000} unit="ft" disabled />
<!-- vertical: description/error keep their place in the field scaffold -->
<RangeSlider name="window" label="Flight window" max={30} unit="m" orientation="vertical" description="Ceiling and floor of the flight." />
<RangeSlider name="angles" label="Angle window" max={45} unit="°" orientation="vertical" error="Pick an angle window." />Props
| Name | Type | Default | Note |
|---|---|---|---|
name | string | — | Required. Base name — the thumbs submit as {name}-min and {name}-max. |
label | string | — | Required. Rendered as the fieldset legend; sr-only with hideLabel. |
min | number | 0 | |
max | number | 100 | |
step | number | 1 | |
valueMin | number | min | Bindable. The lower thumb. |
valueMax | number | max | Bindable. The upper thumb. |
showInput | boolean | true | Renders the pair of exact-entry number fields. |
ticks | SliderTick[] | — | Decorative marks under the track. See SliderTick below. |
unit | string | — | One visual suffix after the pair; rendered aria-hidden. |
minThumbLabel | string | `${label} (minimum)` | Accessible name for the lower thumb (and its number field). |
maxThumbLabel | string | `${label} (maximum)` | Accessible name for the upper thumb (and its number field). |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Vertical uses writing-mode: vertical-lr (bottom-up growth) on both ranges; horizontal is unchanged. |
inputPosition | 'start' | 'end' | 'end' | Logical on both axes. The two exact-entry fields stay one inline cluster, placed above or below the track in vertical rather than stacked to mirror the thumbs. |
description | string | — | Help text below the legend. |
error | string | — | Inline error message; sets the error state. |
required | boolean | false | Legend indicator only. |
disabled | boolean | false | Disables all four inputs. |
hideLabel | boolean | false | |
class | string | — | Merged after the hz-field hz-field--slider hz-field--slider-range classes. |
SliderTick
| Name | Type | Default | Note |
|---|---|---|---|
value | number | — | Required. A bare number is shorthand for an unlabeled tick. Out-of-range ticks are skipped. |
label | string | — | Small text under the mark. |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-field hz-field--slider hz-field--slider-range
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-state | 'default' | 'error' | 'disabled' | The universal field state hook, on the root. Precedence is fixed: error beats disabled. |
data-has-ticks | present when ticks render | On .hz-slider-row, as on Slider. |
data-has-input | present when the number fields show | On .hz-slider-row. Unstyled by the reference theme. |
data-orientation | 'horizontal' | 'vertical' | On .hz-slider-row; always present, as on Slider. Both ranges pick up writing-mode: vertical-lr; direction: rtl. |
data-input-position | 'start' | 'end' | On .hz-slider-row; always present, as on Slider. The min–max pair stays one inline cluster, reordered as a unit. |
Custom properties
| Hook | Values | Styles |
|---|---|---|
--hz-slider-track-height | <length> — default 0.375rem | Track thickness. |
--hz-slider-thumb-size | <length> — default 1.125rem | Thumb width and height. The fill edge tracks the thumb centre, so the fill and ticks stay aligned at any size. |
--hz-slider-length | <length> — default 12rem | The track's block length in vertical orientation. Ignored in horizontal, where the track flexes to fill the row instead. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-field-label | child element | The label. |
.hz-field-required | child element | The required marker. |
.hz-field-description | child element | The hint text. |
.hz-field-error | child element | The error message. |
.hz-slider-row | child element | The track-and-readout row. |
.hz-slider-track | child element | Paints the track and the between-thumbs fill. |
.hz-slider-min | the lower range input | Carries .hz-slider too. |
.hz-slider-max | the upper range input | Carries .hz-slider too. |
.hz-slider-inputs | child element | Wraps the number-field pair (or the readout) so it stays one inline cluster regardless of orientation. |
.hz-slider-number-min | child element | The lower number field. |
.hz-slider-number-max | child element | The upper number field. |
.hz-slider-sep | child element | The dash between the readouts. |
.hz-slider-ticks | child element | The tick strip. |
Accessibility
The group is a <fieldset> whose label renders as the <legend>. Each thumb is a real range input with its own accessible name (minThumbLabel/maxThumbLabel) and native slider semantics. Tab reaches both thumbs, arrow keys step them, and a thumb dragged past its partner clamps rather than crossing.
The number fields carry derived accessible names and never submit. The two thumbs submit as the base name plus -min/-max suffixes.
description and error chain into aria-describedby on both ranges, and an error also sets aria-invalid on both. required renders the legend indicator only. aria-required is not part of the slider role's supported ARIA attributes, so it is never applied.
Ticks, the separator, and the readout are decorative and aria-hidden. In vertical orientation the component stamps aria-orientation="vertical" on both ranges explicitly, since writing-mode does not reliably flip a native range's implicit horizontal orientation in the accessibility tree across engines and assistive tech.
References: APG Slider (Multi-Thumb) pattern · MDN: <input type="range">