icon

Morainev0.5.0

Slider
formsslider

Slider

Range slider component with single or multiple thumbs and step markers.

Import#

import { Slider } from 'moraine'

Slot Structure#

Track with a fill range and one or more draggable thumb handles.

root
├── track
│ ├── range
│ └── divider (optional ×n)
└── thumb (×n)

Examples#

Controlled Single#

Input phase updates with onValueChange and commit phase updates with onChange.

function ControlledSingle() {
const [singleValue, setSingleValue] = createSignal(32)
const [singleCommit, setSingleCommit] = createSignal(32)
return (
<div class="max-w-xl space-y-3">
<Slider
value={singleValue()}
min={0}
max={100}
step={1}
onValueChange={setSingleValue}
onChange={setSingleCommit}
/>
<p class="text-xs text-muted-foreground">Current value: {singleValue()}</p>
<p class="text-xs text-muted-foreground">Last committed value: {singleCommit()}</p>
</div>
)
}

Variants#

Default and bold variants with visual step dividers.

There will be no effective if divider set to true but step does not set.

function Variants() {
return (
<div class="w-lg space-y-5">
<div class="space-y-2">
<label class="text-xs text-muted-foreground block uppercase">Default with divider</label>
<Slider divider min={0} max={100} step={10} defaultValue={40} />
</div>
<div class="space-y-2">
<label class="text-xs text-muted-foreground block uppercase">Bold with divider</label>
<Slider divider variant="bold" min={0} max={100} step={10} defaultValue={30} />
</div>
</div>
)
}

Sizes#

Track and thumb sizing from xs to xl.

function Sizes() {
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
return (
<div class="flex gap-4 w-full">
<div class="flex flex-(1 col) gap-4">
<For each={SIZES}>
{(size) => (
<div class="space-y-2">
<label class="text-xs text-muted-foreground block uppercase">{size}</label>
<Slider size={size} defaultValue={35} />
</div>
)}
</For>
</div>
<div class="flex flex-(1 col) gap-4">
<For each={SIZES}>
{(size) => (
<div class="space-y-2">
<label class="text-xs text-muted-foreground block uppercase">{size}</label>
<Slider variant="bold" size={size} defaultValue={35} />
</div>
)}
</For>
</div>
</div>
)
}

Disabled#

Disabled sliders keep values visible while preventing interaction.

function Disabled() {
return <Slider disabled min={0} max={100} step={10} defaultValue={35} />
}

Orientations and Invert#

Horizontal default layout and vertical layout with fixed container height.

function Orientations() {
const [horizontalValue, setHorizontalValue] = createSignal(45)
const [verticalValue, setVerticalValue] = createSignal(45)
const [inverted, setInverted] = createSignal(false)
const [isBold, setIsBold] = createSignal(false)
return (
<div class="max-w-xl space-y-4">
<Switch label="Invert direction" checked={inverted()} onChange={setInverted} />
<Switch label="Bold variant" checked={isBold()} onChange={setIsBold} />
<div class="gap-8 grid items-start sm:grid-cols-2">
<div class="w-50 space-y-2">
<label class="text-xs text-muted-foreground block">Horizontal: {horizontalValue()}</label>
<Slider
inverted={inverted()}
variant={isBold() ? 'bold' : undefined}
value={horizontalValue()}
onValueChange={setHorizontalValue}
/>
</div>
<div class="space-y-2">
<label class="text-xs text-muted-foreground block">Vertical: {verticalValue()}</label>
<div class="flex h-48 items-center">
<Slider
orientation="vertical"
inverted={inverted()}
variant={isBold() ? 'bold' : undefined}
value={verticalValue()}
onValueChange={setVerticalValue}
/>
</div>
</div>
</div>
</div>
)
}

Range Slider#

Two thumbs with controlled array value, optional minimum gap, and configurable thumb crossing.

function RangeSlider() {
const [rangeValue, setRangeValue] = createSignal<number[]>([20, 75])
const [minStepsBetweenThumbs, setMinStepsBetweenThumbs] = createSignal(0)
const [allowThumbCrossing, setAllowThumbCrossing] = createSignal(true)
return (
<div class="max-w-xl space-y-3">
<Checkbox
checked={allowThumbCrossing()}
onChange={setAllowThumbCrossing}
label="Allow dragging across overlapping thumbs"
/>
<Checkbox
checked={minStepsBetweenThumbs() > 0}
onChange={(isChecked) => setMinStepsBetweenThumbs(isChecked ? 10 : 0)}
label="Min steps between thumbs"
/>
<Slider
value={rangeValue()}
min={0}
max={100}
step={1}
minStepsBetweenThumbs={minStepsBetweenThumbs()}
allowThumbCrossing={allowThumbCrossing()}
onValueChange={(next) => {
if (Array.isArray(next)) {
setRangeValue(next)
}
}}
/>
<p class="text-xs text-muted-foreground w-50">
Range: {rangeValue()[0]} - {rangeValue()[1]}
</p>
<p class="text-xs text-muted-foreground w-50">
Thumb crossing:{' '}
{allowThumbCrossing() && minStepsBetweenThumbs() === 0 ? 'Enabled' : 'Constrained'}
</p>
<p class="text-xs text-muted-foreground w-50">Min steps between: {minStepsBetweenThumbs()}</p>
</div>
)
}

Form Integration#

Submit to validate required minimum value through Form + FormField.

function FormIntegration() {
const [formState, setFormState] = createSignal({
volume: 10,
})
const updateFormVolume = (nextValue: SliderT.Value) => {
const next = Array.isArray(nextValue) ? (nextValue[0] ?? 0) : nextValue
setFormState((prev) => ({ ...prev, volume: next }))
}
const form = createForm({
schema: v.object({ volume: v.pipe(v.number(), v.minValue(20, 'Volume must be at least 20.')) }),
initialInput: untrack(formState),
validate: 'input',
})
return (
<Form of={form}>
<div class="max-w-xl space-y-4">
<FormField name="volume" label="Volume" description="Keep it at least 20.">
<Slider value={formState().volume} onValueChange={updateFormVolume} />
</FormField>
<div class="flex gap-3 items-center">
<Button type="submit" variant="secondary" size="sm">
Validate
</Button>
<p class="text-xs text-muted-foreground">Current volume: {formState().volume}</p>
</div>
</div>
</Form>
)
}

API Reference#

Attributes#

Slotroot6 attributes
Slider container that owns track, range, thumbs, and labels.

Data Attributes

5
Data AttributeDescription
data-disabled
Present when the component or item is disabled.
data-invalid
Present when the field has a validation error.
data-orientation
Stores the rendered orientation.
data-readonly
Present when the field is read-only.
data-required
Present when the field is required.

ARIA Attributes

1
ARIA AttributeDescription
role
Defines the semantic role exposed to assistive technology.

Props#

PropTypeDefaultDescription
allowThumbCrossingboolean | undefinedtrue
Whether dragging can continue across another thumb when there is no minimum gap.
classClassValue
Class applied to the component root or trigger element.
classesSliderT.Classes | undefined
defaultValueTValue | undefined
The default value of the input (uncontrolled).
disabledboolean | undefinedfalse
Whether the input is disabled.
dividerboolean | undefinedfalse
Whether to show visual step dividers on the track, only applicable when `step` is defined and greater than 0.
idstring | undefined
The ID of the input element.
invertedboolean | undefined
maxnumber | undefined100
Maximum value of the slider.
minnumber | undefined0
Minimum value of the slider.
minStepsBetweenThumbsnumber | undefined0
Minimum steps required between thumbs in a multi-thumb slider.
namestring | undefined
The name of the input element, used for form submission.
onChange((value: TValue) => void) | undefined
Callback when the slider selection change is committed.
onValueChange((value: TValue) => void) | undefined
Callback when the slider selection changes during interaction.
orientation"horizontal" | "vertical" | undefined
readOnlyboolean | undefinedfalse
Whether the input is read-only.
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
requiredboolean | undefinedfalse
Whether the input is required.
size"xs" | "sm" | "md" | "lg" | "xl" | undefined
stepnumber | undefined
Step increment between values. When omitted, pointer movement is continuous.
styleJSX.CSSProperties | undefined
stylesSliderT.Styles | undefined
valueTValue | undefined
The current value of the input (controlled).
variant"default" | "bold" | undefined