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 each into your own Model, init, Message,
// update, view, and subscription definitions.
import { Match as M, Option, Schema as S } from 'effect'
import { Command, Subscription } from 'foldkit'
import { html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

import { Slider } from '@foldkit/ui'

// Add a field to your Model for the Slider Submodel:
const Model = S.Struct({
  ratingDemo: Slider.Model,
  // ...your other fields
})

// In your init function, initialize the Slider Submodel with min / max /
// step and a unique id:
const init = () => [
  {
    ratingDemo: 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: Slider.Message,
})

// Inside your update function's M.tagsExhaustive({...}), delegate to
// Slider.update. The OutMessage's `ChangedValue` carries the new
// number. Lift it to domain state, validate, or persist on each commit.
GotSliderMessage: ({ message }) => {
  const [nextSlider, commands, maybeOutMessage] = Slider.update(
    model.ratingDemo,
    message,
  )
  const mappedCommands = Command.mapMessages(commands, message =>
    GotSliderMessage({ message }),
  )

  return Option.match(maybeOutMessage, {
    onNone: () => [
      evo(model, { ratingDemo: () => nextSlider }),
      mappedCommands,
    ],
    onSome: M.type<Slider.OutMessage>().pipe(
      M.tagsExhaustive({
        ChangedValue: ({ value }) => [
          // The child has emitted `ChangedValue`. The body commits
          // the child's next state as usual. In this arm the parent
          // can also update its own state or dispatch its own
          // Commands, for example persist the value, validate, or
          // trigger a downstream Command.
          evo(model, { ratingDemo: () => nextSlider }),
          mappedCommands,
        ],
      }),
    ),
  })
}

// NOTE: wire BOTH dragPointer and dragEscape. Without dragEscape, pressing
// Escape during a drag won't cancel back to the origin value, but every
// other drag mechanic still works. Silent partial breakage.
const sliderSubscriptions = Subscription.lift({
  sliderPointer: Slider.subscriptions.dragPointer,
  sliderEscape: Slider.subscriptions.dragEscape,
})<Model, Message>({
  toChildModel: model => model.ratingDemo,
  toParentMessage: message => GotSliderMessage({ message }),
})

const subscriptions = Subscription.aggregate<Model, Message>()(
  sliderSubscriptions,
  // ...your other subscription records
)

// 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:
const view = (model: Model) => {
  const h = html<Message>()

  return h.submodel({
    slotId: 'rating',
    model: model.ratingDemo,
    view: Slider.view,
    viewInputs: {
      formatValue: value => `${String(value)} of 10`,
      toView: attributes =>
        h.div(
          [h.Class('flex flex-col gap-2 w-full max-w-sm')],
          [
            h.div(
              [h.Class('flex items-center justify-between text-sm')],
              [
                h.label(
                  [...attributes.label, h.Class('font-medium')],
                  ['Rating'],
                ),
                h.span(
                  [h.Class('tabular-nums text-gray-600')],
                  [`${String(model.ratingDemo.value)} / 10`],
                ),
              ],
            ),
            h.div(
              [
                ...attributes.root,
                h.Class('relative h-6 w-full flex items-center'),
              ],
              [
                h.div(
                  [
                    ...attributes.track,
                    h.Class('h-1.5 w-full rounded-full bg-gray-200'),
                  ],
                  [
                    h.div(
                      [
                        ...attributes.filledTrack,
                        h.Class('h-full rounded-full bg-blue-600'),
                      ],
                      [],
                    ),
                  ],
                ),
                h.div(
                  [
                    ...attributes.thumb,
                    h.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',
                    ),
                  ],
                  [],
                ),
              ],
            ),
          ],
        ),
    },
    toParentMessage: message => GotSliderMessage({ message }),
  })
}

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.
getTrackRoot(() => Document | ShadowRoot) | undefined-Optional accessor returning the DOM root that contains the slider track. Defaults to `document`. Override when rendering inside a Shadow DOM so the drag subscription can find the track element to measure cursor position.

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.

OutMessage

Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Parents pattern-match on the OutMessage in their own update handler.

NameTypeDefaultDescription
ChangedValue{ value: number }-Emitted whenever the slider value changes via drag, click-to-jump, or keyboard navigation. Pattern-match the third tuple element of Slider.update in your GotSliderMessage handler to react: persist the value, validate, or trigger a downstream Command.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson