Skip to content

Slider

A primitive for building slider inputs. Supports single-value and range (two-thumb) modes, keyboard navigation with configurable step sizes, CSS-driven visual range fill, and thumb alignment strategies for different visual systems.

import { Slider } from "@areia/slots";

document.querySelector("#root")!.innerHTML = `
  <div data-slot="slider" class="relative h-8 w-72">
    <div data-slot="slider-track" class="win95-inset absolute top-1/2 h-2 w-full -translate-y-1/2 bg-white">
      <div data-slot="slider-range" class="h-full bg-blue-900"></div>
    </div>
    <div data-slot="slider-thumb" class="size-5 border-2 border-t-white border-l-white border-r-black border-b-black bg-[#c0c0c0]"></div>
  </div>
`;

const root = document.querySelector('[data-slot="slider"]')!;
const controller = Slider.createSlider(root, {
  defaultValue: 50,
  min: 0,
  max: 100,
  step: 1,
});

Import

import { Slider } from "@areia/slots";

Usage

import { Slider } from "@areia/slots";

// Auto-bind a single root
const root = document.querySelector('[data-slot="slider"]');
const controller = Slider.createSlider(root, {
  defaultValue: 50,
  min: 0,
  max: 100,
  step: 1,
  largeStep: 10,
  onValueChange: (value) => console.log("value:", value),
  onValueCommit: (value) => console.log("committed:", value),
});

// Auto-bind all unbound roots in scope
const controllers = Slider.create();

Expected Markup

The slider requires a track and one or two thumbs inside the root. The root (or an explicit slider-control wrapper) should be positioned so the absolutely positioned thumb is constrained to the track width. An optional range element visualizes the filled portion between the start and the thumb(s). For range mode, use two thumbs.

<!-- Single value -->
<div data-slot="slider" style="position: relative">
  <div data-slot="slider-track">
    <div data-slot="slider-range"></div>
  </div>
  <div data-slot="slider-thumb"></div>
</div>

<!-- Range (two-value) -->
<div data-slot="slider" style="position: relative">
  <div data-slot="slider-track">
    <div data-slot="slider-range"></div>
  </div>
  <div data-slot="slider-thumb"></div>
  <div data-slot="slider-thumb"></div>
</div>

Range mode is indicated by providing a comma-separated defaultValue (e.g. "25,75") or a two-element array [25, 75].

Data Slots

SlotRequiredDescription
sliderYesRoot container. Supports data-disabled.
slider-trackYesThe track along which the thumb(s) slide.
slider-rangeNoVisual element showing the filled range. Width/height set via inline style.
slider-thumbYesDraggable thumb handle. Use one for single, two for range mode.

Generated Attributes

The controller writes the following attributes on initialization and updates them as state changes:

AttributeElement(s)Description
data-orientationRoot, control, track, range, thumb"horizontal" or "vertical".
data-disabledRoot, control, track, range, thumbPresent when the slider is disabled.
data-draggingRoot, active thumbPresent while a thumb is being actively dragged.
data-indexslider-thumbZero-based index of the thumb in range mode.
aria-valueminslider-thumbMinimum value for this thumb.
aria-valuemaxslider-thumbMaximum value for this thumb.
aria-valuenowslider-thumbCurrent value of this thumb.
aria-orientationslider-thumb"horizontal" or "vertical".

Range Element

The slider-range element receives inline width/height and positional styles (e.g. left, right, top, bottom) to visually indicate the filled portion of the track. For a single thumb, the range spans from the track start to the thumb. For range mode, it spans between the two thumbs.

Events

All events are dispatched from the root element (data-slot="slider").

Outbound

EventDetailDescription
slider:change{ value: number | [number, number] }Fired on every value change during drag, keyboard, or programmatic update.
slider:commit{ value: number | [number, number] }Fired when the user finishes interacting (drag end, key release).

Inbound

EventDetailDescription
slider:set{ value: number | [number, number] }Dispatch on the root to programmatically set the value.

Send slider:set on the root element to update the value without a controller reference:

const root = document.querySelector('[data-slot="slider"]');
root?.dispatchEvent(
  new CustomEvent("slider:set", {
    bubbles: true,
    detail: { value: 75 },
  }),
);

API Reference

Options

Slider.createSlider(root, options)

OptionTypeDefaultDescription
defaultValuenumber | [number, number]Initial value. A two-element array or comma-separated string (e.g. "25,75") enables range mode.
minnumber0Minimum allowed value.
maxnumber100Maximum allowed value.
stepnumber1Step increment for arrow keys and dragging.
largeStepnumber10Step increment for PageUp/PageDown and Shift+arrow keys.
orientation"horizontal" | "vertical""horizontal"Layout direction. Controls movement axis and ARIA attributes.
thumbAlignment"center" | "edge" | "edge-client-only""center"How the thumb aligns to the track value. See behavior section for details.
disabledbooleanfalseDisable the entire slider.
onValueChange(value: number | [number, number]) => voidCalled on every value change.
onValueCommit(value: number | [number, number]) => voidCalled when the user finishes interacting (drag end, key release).

Controller

MemberTypeDescription
valuenumber | [number, number] (getter)Current value. Range mode returns a two-element array.
minnumber (getter)Minimum value.
maxnumber (getter)Maximum value.
disabledboolean (getter)Whether the slider is disabled.
setValue(value)(value: number | [number, number]) => voidSet the value programmatically.
destroy()() => voidRemove all event listeners and clean up.

Controller

The controller is returned by Slider.createSlider() and provides imperative control over the slider's value.

const controller = Slider.createSlider(root, {
  defaultValue: 50,
  min: 0,
  max: 100,
  step: 1,
});

// Read current value
console.log(controller.value); // 50 (single) or [25, 75] (range)

// Read constraints
console.log(controller.min); // 0
console.log(controller.max); // 100
console.log(controller.disabled); // false

// Set value programmatically
controller.setValue(75);
// In range mode:
controller.setValue([30, 70]);

// Tear down
controller.destroy();

Behavior

  • Keyboard stepping: Arrow keys move the thumb by step. PageUp/PageDown and Shift+Arrow move by largeStep. Home jumps to min, End jumps to max.
  • Thumb alignment: The thumbAlignment option controls how the thumb maps to the track value:
    • "center" (default): Positions the thumb at the raw percentage and translates it by half its size, so the thumb center sits on the value.
    • "edge": Insets the travel area by the thumb size, keeping the thumb fully inside the track at the min/max edges.
    • "edge-client-only": Alias for "edge" for Base UI compatibility.
  • Drag: Thumbs can be dragged with mouse or touch along the track, constrained by min, max, and step.
  • Range mode: When two thumbs are present (or defaultValue is a two-element array / comma-separated string), the slider operates in range mode. The two thumbs are independently draggable and never cross each other.
  • Range fill: The slider-range element receives inline width (horizontal) or height (vertical) plus positional styles to render the filled region between the thumbs or from the track start to a single thumb.