On this pageOverview
Slider
A numeric range input for values that sit on a continuous or stepped scale. Common uses include rating scales, volume controls, filter thresholds, and brightness settings. Follows the WAI-ARIA slider pattern with role="slider", full keyboard navigation, and pointer drag.
See it in an app
Check out how Slider is wired up in a real Foldkit app.
Slider is headless — your toView callback controls all markup and styling. The component hands back attribute groups for the root, track, filled track, thumb, label, and an optional hidden input for form submission.
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt — fit them into your own Model, init, Message,
// update, view, and subscription definitions.
import { Effect, Schema as S, Stream } from 'effect'
import { Command, Subscription, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, div, label, span } from './html'
// Add a field to your Model for the Slider Submodel:
const Model = S.Struct({
ratingDemo: Ui.Slider.Model,
// ...your other fields
})
// In your init function, initialize the Slider Submodel with min / max /
// step and a unique id:
const init = () => [
{
ratingDemo: Ui.Slider.init({
id: 'rating',
min: 0,
max: 10,
step: 1,
initialValue: 3,
}),
// ...your other fields
},
[],
]
// Embed the Slider Message in your parent Message:
const GotSliderMessage = m('GotSliderMessage', {
message: Ui.Slider.Message,
})
// Inside your update function's M.tagsExhaustive({...}), delegate to Slider.update:
GotSliderMessage: ({ message }) => {
const [nextSlider, commands] = Ui.Slider.update(model.ratingDemo, message)
return [
// Merge the next state into your Model:
evo(model, { ratingDemo: () => nextSlider }),
// Forward the Submodel's Commands through your parent Message:
commands.map(
Command.mapEffect(Effect.map(message => GotSliderMessage({ message }))),
),
]
}
// Wire the Slider's document-level drag subscriptions into your app's
// SubscriptionDeps and subscriptions. This is what powers pointer drag and
// Escape-to-cancel:
const sliderFields = Ui.Slider.SubscriptionDeps.fields
const SubscriptionDeps = S.Struct({
sliderPointer: sliderFields['documentPointer'],
sliderEscape: sliderFields['documentEscape'],
// ...your other subscription deps
})
const sliderSubscriptions = Ui.Slider.subscriptions
const subscriptions = Subscription.makeSubscriptions(SubscriptionDeps)<
Model,
Message
>({
sliderPointer: {
modelToDependencies: model =>
sliderSubscriptions.documentPointer.modelToDependencies(model.ratingDemo),
dependenciesToStream: (dependencies, readDependencies) =>
sliderSubscriptions.documentPointer
.dependenciesToStream(dependencies, readDependencies)
.pipe(Stream.map(message => GotSliderMessage({ message }))),
},
sliderEscape: {
modelToDependencies: model =>
sliderSubscriptions.documentEscape.modelToDependencies(model.ratingDemo),
dependenciesToStream: (dependencies, readDependencies) =>
sliderSubscriptions.documentEscape
.dependenciesToStream(dependencies, readDependencies)
.pipe(Stream.map(message => GotSliderMessage({ message }))),
},
})
// Inside your view function, render the slider. You control every element's
// markup and classes through the `toView` callback. The `attributes` groups
// provide ARIA, pointer, and keyboard wiring:
Ui.Slider.view({
model: model.ratingDemo,
toParentMessage: message => GotSliderMessage({ message }),
formatValue: value => `${String(value)} of 10`,
toView: attributes =>
div(
[Class('flex flex-col gap-2 w-full max-w-sm')],
[
div(
[Class('flex items-center justify-between text-sm')],
[
label([...attributes.label, Class('font-medium')], ['Rating']),
span(
[Class('tabular-nums text-gray-600')],
[`${String(model.ratingDemo.value)} / 10`],
),
],
),
div(
[...attributes.root, Class('relative h-6 w-full flex items-center')],
[
div(
[
...attributes.track,
Class('h-1.5 w-full rounded-full bg-gray-200'),
],
[
div(
[
...attributes.filledTrack,
Class('h-full rounded-full bg-blue-600'),
],
[],
),
],
),
div(
[
...attributes.thumb,
Class(
'h-5 w-5 rounded-full bg-white border-2 border-blue-600 shadow cursor-grab focus-visible:ring-2 focus-visible:ring-blue-600 data-[dragging]:cursor-grabbing',
),
],
[],
),
],
),
],
),
})Pointer drag needs document-level pointermove / pointerup tracking (the cursor can leave the slider element). Slider exposes this as a Subscription you wire into your app’s subscriptions alongside an Escape-key subscription that cancels an in-progress drag. The example snippet above shows the full wiring.
Slider exposes data-dragging while the user is actively dragging, data-disabled when disabled, and data-orientation on the root. The filledTrack attribute group carries an inline width so the filled portion always matches the current value.
| Attribute | Condition |
|---|---|
data-dragging | Present on the root, track, filled track, and thumb while the user is actively dragging. |
data-disabled | Present on all groups when isDisabled is true. |
data-orientation | Present on the root. Always "horizontal" in v1; vertical is planned. |
| Key | Description |
|---|---|
| ArrowRight / ArrowUp | Increases the value by one step. |
| ArrowLeft / ArrowDown | Decreases the value by one step. |
| PageUp | Increases the value by ten steps. |
| PageDown | Decreases the value by ten steps. |
| Home | Jumps to the minimum value. |
| End | Jumps to the maximum value. |
| Escape | During a pointer drag, cancels the drag and restores the pre-drag value. |
The thumb receives role="slider", aria-valuemin, aria-valuemax, aria-valuenow, and aria-orientation. When formatValue is provided, the formatted string is announced via aria-valuetext. By default the thumb is labeled via aria-labelledby pointing at the id carried on the label attribute group; you can override this with an explicit ariaLabel or ariaLabelledBy.
Configuration object passed to Slider.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the slider instance. |
min | number | - | Minimum value. |
max | number | - | Maximum value. |
step | number | - | Increment between allowed values. Fractional steps are rounded to the step’s decimal precision to avoid floating-point drift. |
initialValue | number | - | Initial value. Clamped to [min, max] and snapped to the nearest step. |
Configuration object passed to Slider.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Slider.Model | - | The slider state from your parent Model. |
toParentMessage | (childMessage: Slider.Message) => ParentMessage | - | Wraps Slider Messages in your parent Message type for Submodel delegation. |
toView | (attributes: SliderAttributes) => Html | - | Callback that receives attribute groups for the root, track, filled track, thumb, label, and hidden input elements. |
ariaLabel | string | - | Accessible name for screen readers when there is no visible label. |
ariaLabelledBy | string | - | ID of an external element whose text serves as the slider’s accessible name. |
formatValue | (value: number) => string | - | Produces the aria-valuetext announced to screen readers. Use it when the numeric value needs a natural-language form (e.g. "3 of 10" or "50 percent"). |
isDisabled | boolean | false | Whether the slider is disabled. Removes pointer and keyboard interactivity while preserving focusability. |
name | string | - | Form field name. When provided, a hidden input carrying the current numeric value is included for native form submission. |
Attribute groups provided to the toView callback.
| Name | Type | Default | Description |
|---|---|---|---|
root | ReadonlyArray<Attribute<Message>> | - | Spread onto the outer wrapper. Carries data-slider-id, data-orientation, and state data attributes. |
track | ReadonlyArray<Attribute<Message>> | - | Spread onto the track element (the bar). Carries data-slider-track-id (used by the drag subscription to measure cursor position), positioning styles, and the pointerdown handler for click-to-jump. |
filledTrack | ReadonlyArray<Attribute<Message>> | - | Spread onto an element nested inside the track. Its inline width reflects the current value as a percentage of the range. |
thumb | ReadonlyArray<Attribute<Message>> | - | Spread onto the draggable handle. Carries role="slider", tabindex, aria-value*, the pointerdown handler, the keyboard handler, and positioning. |
label | ReadonlyArray<Attribute<Message>> | - | Spread onto the visible label element. Carries the id the thumb’s aria-labelledby points to by default. |
hiddenInput | ReadonlyArray<Attribute<Message>> | - | Spread onto a hidden <input> for form submission. Only populated when the name prop is set. |