Slider
A labeled range slider paired with a synced number field for fine-tuned keyboard entry. Typed values commit on change, snapped to the step and clamped to the range.
Import
import { Slider } from "@hyzer-labs/ui"Demo
Drag for coarse control, type for precision. The number field commits on blur or Enter,
clamped to the range. Try typing 900.
let distance = $state(350);
<Slider name="distance" label="Longest throw" min={0} max={700} unit="ft" bind:value={distance} />step applies to both inputs: the arrows step by it, and a typed value snaps
to the step grid anchored at min. Here it's step={0.5}, so
type 172.3 and it commits as 172.5.
let weight = $state(172.5);
<Slider name="weight" label="Disc weight" min={160} max={175} step={0.5} unit="g" bind:value={weight} />Ticks are visual marks only. The thumb does not snap to them: stepping stays on the step grid. Bare numbers make unlabeled marks, and { value, label } adds the caption. For a min–max interval instead of a
single value, see RangeSlider, which shares
this ticks API.
<Slider
name="speed"
label="Disc speed"
min={1}
max={14}
ticks={[
{ value: 1, label: 'putter' },
{ value: 5, label: 'mid' },
{ value: 9, label: 'fairway' },
{ value: 12, label: 'distance' }
]}
/>showInput={false} swaps the number field for a read-only readout. The value
stays visible for coarse-only settings, and the range itself keeps full keyboard support.
<Slider name="wind" label="Wind strength" min={0} max={10} value={4} showInput={false} />orientation="vertical" switches the track to a native writing-mode: vertical-lr range. It grows bottom-up, so Up and Right arrows
still increase toward max. Ticks and labels sit beside the track. The
track's block length comes from --hz-slider-length (default 12rem), so a vertical slider never grows the page unbounded.
inputPosition is logical on both axes: "end" (default) puts
the number field below the track, "start" puts it above. Placing several
vertical sliders side by side is up to you. This demo wraps them in Cluster.
<Cluster gap="lg">
<Slider
name="elevation"
label="Elevation"
min={0}
max={2000}
unit="ft"
orientation="vertical"
ticks={[
{ value: 0, label: 'sea level' },
{ value: 1000, label: 'ridge' },
{ value: 2000, label: 'peak' }
]}
/>
<Slider
name="elevation-start"
label="Elevation (input above)"
min={0}
max={2000}
unit="ft"
orientation="vertical"
inputPosition="start"
/>
</Cluster>description and error are announced with the range: both chain
into aria-describedby. Error and disabled are also field states. The
wrapper's data-state reflects them, and error wins.
<Slider
name="rating"
label="Target rating"
min={700}
max={1100}
step={5}
description="Round ratings run roughly 700–1100."
/>
<Slider name="stack" label="Discs in your bag" max={30} error="You need at least one disc." />
<Slider name="entry" label="Entry fee split" required />
<Slider name="locked" label="Course length (locked)" min={4000} max={12000} value={7245} unit="ft" disabled />
<!-- vertical: description/error keep their place in the field scaffold -->
<Slider name="apex" label="Apex height" max={30} unit="m" orientation="vertical" description="Peak height of the throw." />
<Slider name="angle" label="Release angle" max={45} unit="°" orientation="vertical" error="Pick an angle." />Props
| Name | Type | Default | Note |
|---|---|---|---|
name | string | — | Required. Form field name (on the range input). |
label | string | — | Required. Labels the range; sr-only with hideLabel. |
min | number | 0 | |
max | number | 100 | |
step | number | 1 | |
value | number | min | Bindable. |
showInput | boolean | true | Renders the exact-entry number field next to the slider. |
ticks | SliderTick[] | — | Decorative marks under the track. See SliderTick below. |
unit | string | — | Visual suffix after the number field; rendered aria-hidden. |
inputLabel | string | `${label} (exact value)` | Accessible name for the number field. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Vertical uses writing-mode: vertical-lr (bottom-up growth); horizontal is unchanged. |
inputPosition | 'start' | 'end' | 'end' | Logical on both axes: horizontal trailing/leading, vertical below/above the track. |
description | string | — | Help text below the label. |
error | string | — | Inline error message; sets the error state. |
required | boolean | false | Label indicator only. |
disabled | boolean | false | Disables both inputs. |
hideLabel | boolean | false | |
class | string | — | Merged after the hz-field hz-field--slider 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
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; reserves room for the tick strip. Reflects the normalized list — out-of-range ticks are dropped — so it is not derivable from the prop. |
data-has-input | present when the number field shows | On .hz-slider-row. The reference theme targets the parts instead, so this one is yours. |
data-orientation | 'horizontal' | 'vertical' | On .hz-slider-row; always present. Drives the writing-mode-based vertical mechanism and the theme rotation (fill, ticks). Default horizontal. |
data-input-position | 'start' | 'end' | On .hz-slider-row; always present. Logical on both axes — reorders the track vs. the number field/readout. Default end. |
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 | The painted track. |
.hz-slider | the range input | The native input carrying the thumb. |
.hz-slider-ticks | child element | The tick strip. |
.hz-slider-tick | child element | One tick. |
.hz-slider-tick-label | child element | A tick’s label. |
.hz-slider-number | child element | The editable number field. |
.hz-slider-value | child element | The read-only readout. |
.hz-slider-unit | child element | The unit suffix. |
Accessibility
The range input is the labeled (id/for), named, form-participating control. Its slider semantics (value, min, max) are native, and arrow keys step it.
The number field is an exact-entry affordance with its own accessible name (inputLabel) and no name, so it never submits.
description and error chain into aria-describedby on the range (description first), and an error sets aria-invalid. required renders the label indicator only. aria-required is not part of the slider role's supported ARIA attributes, so it is never applied.
unit is decorative and aria-hidden. Put meaning-bearing units in the label or description instead. In vertical orientation the component stamps aria-orientation="vertical" on the range 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 pattern · MDN: <input type="range">