Slider#

A number — or number[] — model with two projections over one anatomy. A scalar model renders Slider.Control, a native <input type="range"> that brings its own keyboard, form participation and accessibility. An array model composes the real Slider.Track / Slider.Range / Slider.Thumb parts, one APG role="slider" thumb per value. Zero positions the moving parts structurally and paints nothing.

Import#

TSX
import { Slider } from '@sigx/zero/slider';

Slider is a compound: Slider.Root, Slider.Label, Slider.Control, Slider.Track, Slider.Range, Slider.Thumb, Slider.ValueText. It is also re-exported from the @sigx/zero root, together with sliderAnatomy, useSliderContext and the SliderMark type.

Usage#

TSX
import { component } from 'sigx';
import { Slider } from '@sigx/zero/slider';

const Volume = component(({ signal }) => {
    const state = signal({ volume: 40 });

    return () => (
        <Slider.Root model={() => state.volume} min={0} max={100} name="volume">
            <Slider.Label>Volume</Slider.Label>
            <Slider.Control />
            <Slider.ValueText />
        </Slider.Root>
    );
});

model={() => state.volume} binds the value both ways. Leave the model off and pass defaultValue to keep the state inside the component; valueChange fires either way, and emission preserves the model's shape — scalar in, scalar out. See Models.

This is the native projection: the platform supplies the keyboard, posts the value under name through the input itself, and a design system styles the thumb and track through the input's vendor pseudo-elements against [data-scope="slider"][data-part="control"]. The current fraction is published as --slider-percent on the root for track-fill styling.

A range with two thumbs#

TSX
const Price = component(({ signal }) => {
    const state = signal({ price: [100, 400] });

    return () => (
        <Slider.Root
            model={() => state.price}
            min={0}
            max={500}
            step={10}
            name="price"
            marks={[0, { value: 250, label: '250' }, 500]}
            getValueText={(v) => `$${v}`}
        >
            <Slider.Label>Price</Slider.Label>
            <Slider.Track>
                <Slider.Range />
                <Slider.Thumb label="Minimum price" />
                <Slider.Thumb label="Maximum price" />
            </Slider.Track>
            <Slider.ValueText />
        </Slider.Root>
    );
});

With a number[] model you compose the parts: one Slider.Thumb per value, in order — thumbs claim their index by registration order, and an explicit index pins one. Thumbs cannot cross: a thumb clamps at its neighbour, and announces that clamp as its aria-valuemin / aria-valuemax (the allowed range, not the rail's). Slider.Range spans the lowest to the highest value (min to the value when single). A pointer press on the track moves the nearest thumb there and starts a drag; a press on a thumb drags that thumb even when two are stacked.

A range model has no native widget to post through, so it renders one hidden input per value under the shared name — how multi-value fields post.

Marks#

marks renders one positioned mark part per entry inside the track. A bare number is a tick; { value, label } also renders the label text inside the part.

Anatomy#

PartElementStatesFlagsNotes
rootdivdisabled, invalid, focus-visibleCarries the variant axes and data-orientation. Publishes --slider-percent.
labellabeldisabledfor the control id. Inside root.
controlinputdisabled, invalid, focus-visible, pressedtype="range"; the scalar projection. Carries name, min, max, step, aria-invalid.
trackdivdisabledThe rail; position: relative. Pointer presses move the nearest thumb.
rangedivdisabledThe filled span, positioned with inset-inline-start and inline-size percents. Inside track.
thumbdivdisabled, pressed, focus-visiblerole="slider", tabIndex=0, aria-valuemin / aria-valuemax / aria-valuenow / aria-valuetext, aria-label. Positioned with inset-inline-start. Inside track.
markspandisabledOne per marks entry, positioned with inset-inline-start; carries the label text. Inside track.
value-textoutputfor the control id; defaults to the value, or the values joined with an en dash.
hidden-inputinputtype="hidden"; one per value under name, rendered for array models only.

Every part carries data-scope="slider" and data-part="<part>". One recipe carries both projections — rules for parts a render does not include are inert there. Zero positions range, thumb and mark absolutely with logical inset-inline-start percents, so RTL mirrors for free, and paints nothing: a recipe centres the thumb on its position with a negative margin-inline-start of half its own width, and owns every colour. --slider-percent tracks the highest value's fraction in both projections.

data-pressed is the only press-feedback flag on this scope: a drag is a long press with no one-shot, so the control and thumb carry pressed while the pointer is down — including after it leaves the element, until a window-level release — and never press-animating. See The anatomy contract.

Props#

Slider.Root#

PropTypeDefaultDescription
modelnumber | number[]Two-way binding of the value; an array selects the composed projection.
defaultValuenumber | number[]minInitial value when uncontrolled.
valueChangeevent (value: number | number[])Fires whenever any value changes; same shape as the model.
minnumber0Lower bound.
maxnumber100Upper bound.
stepnumber1Quantisation; values are snapped to the step, anchored at min.
namestringForm field name; on the native control, or one hidden input per value.
invalidbooleanfalseRenders data-invalid and aria-invalid.
disabledbooleanfalseInert; renders data-disabled on every part.
marksreadonly SliderMark[][]Ticks rendered as mark parts inside the track; number | { value, label? }.
getValueText(value: number, index: number) => stringPer-thumb aria-valuetext — "$40", "40 percent".
color / size / variant / axes / modsdesign-system vocabularyThe variant axes, rendered as data-* on root.
classstringExtra classes on the root element.

Field context (disabled, invalid, the control and label ids) is merged with the props.

Slider.Label, Slider.Control, Slider.Track, Slider.Range#

Only class. Slider.Track renders the mark parts before its children.

Slider.Thumb#

PropTypeDefaultDescription
indexnumberregistration orderWhich value this thumb drives.
labelstringaria-label; a multi-thumb slider must name each thumb.
classstringExtra classes.

Slider.ValueText#

Only class. Its default slot receives { value, values }; without one it renders the scalar value, or the values joined with an en dash.

Keyboard#

The native control's keyboard is the platform's. Each composed thumb is its own tab stop:

KeyAction
ArrowRight / ArrowUpIncrease by step (ArrowRight flips under RTL).
ArrowLeft / ArrowDownDecrease by step (ArrowLeft flips under RTL).
PageUp / PageDownIncrease / decrease by ten steps.
Home / EndJump to min / max, clamped at the neighbouring thumb.

In the shipped design systems#

Both @sigx/zero-basic and @sigx/zero-daisyui wire color (all eight recommended roles) and size (xsxl) on slider, so <Slider.Root color="accent" size="lg"> is styled in both. Neither wires a variant on the scope; under a design system's /register import the prop is therefore absent. Both style the native control's pseudo-elements and the composed track, range, thumb and mark parts from the same recipe, reading --slider-percent for the native track fill. See Typed vocabulary.

Number Input for a typed value, Progress for a read-only fraction, Field for labelling.