Skip to main content
On this pageOverview

Slider

Overview

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.

Examples

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.

3 of 10
50%
// 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',
                ),
              ],
              [],
            ),
          ],
        ),
      ],
    ),
})

Subscriptions

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.

Styling

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.

AttributeCondition
data-draggingPresent on the root, track, filled track, and thumb while the user is actively dragging.
data-disabledPresent on all groups when isDisabled is true.
data-orientationPresent on the root. Always "horizontal" in v1; vertical is planned.

Keyboard Interaction

KeyDescription
ArrowRight / ArrowUpIncreases the value by one step.
ArrowLeft / ArrowDownDecreases the value by one step.
PageUpIncreases the value by ten steps.
PageDownDecreases the value by ten steps.
HomeJumps to the minimum value.
EndJumps to the maximum value.
EscapeDuring a pointer drag, cancels the drag and restores the pre-drag value.

Accessibility

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.

API Reference

InitConfig

Configuration object passed to Slider.init().

NameTypeDefaultDescription
idstring-Unique ID for the slider instance.
minnumber-Minimum value.
maxnumber-Maximum value.
stepnumber-Increment between allowed values. Fractional steps are rounded to the step’s decimal precision to avoid floating-point drift.
initialValuenumber-Initial value. Clamped to [min, max] and snapped to the nearest step.

ViewConfig

Configuration object passed to Slider.view().

NameTypeDefaultDescription
modelSlider.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.
ariaLabelstring-Accessible name for screen readers when there is no visible label.
ariaLabelledBystring-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").
isDisabledbooleanfalseWhether the slider is disabled. Removes pointer and keyboard interactivity while preserving focusability.
namestring-Form field name. When provided, a hidden input carrying the current numeric value is included for native form submission.

SliderAttributes

Attribute groups provided to the toView callback.

NameTypeDefaultDescription
rootReadonlyArray<Attribute<Message>>-Spread onto the outer wrapper. Carries data-slider-id, data-orientation, and state data attributes.
trackReadonlyArray<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.
filledTrackReadonlyArray<Attribute<Message>>-Spread onto an element nested inside the track. Its inline width reflects the current value as a percentage of the range.
thumbReadonlyArray<Attribute<Message>>-Spread onto the draggable handle. Carries role="slider", tabindex, aria-value*, the pointerdown handler, the keyboard handler, and positioning.
labelReadonlyArray<Attribute<Message>>-Spread onto the visible label element. Carries the id the thumb’s aria-labelledby points to by default.
hiddenInputReadonlyArray<Attribute<Message>>-Spread onto a hidden <input> for form submission. Only populated when the name prop is set.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson