Slider
A control for choosing a numeric value or range by dragging a thumb.
@data-slot/sliderSource ↗bun add @data-slot/slidernpm install @data-slot/sliderpnpm add @data-slot/sliderAnatomy
The thumb count defines the slider: one thumb for a single value, two for a range. slider-control is an optional wrapper for when you need another box to style.
<!-- Single value slider -->
<div data-slot="slider" data-default-value="50">
<div data-slot="slider-track">
<div data-slot="slider-range"></div>
</div>
<div data-slot="slider-thumb"></div>
</div>
<!-- Optional control wrapper -->
<div data-slot="slider" data-default-value="50">
<div data-slot="slider-control">
<div data-slot="slider-track">
<div data-slot="slider-range"></div>
</div>
<div data-slot="slider-thumb"></div>
</div>
</div>
<!-- Range slider (two thumbs) -->
<div data-slot="slider" data-default-value="25,75">
<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>
Examples
Drag to set a value
Show code
<div data-slot="slider" data-default-value="50" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-default-value="50" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Show code
<div data-slot="slider" data-default-value="25" data-thumb-alignment="edge" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-default-value="25" data-thumb-alignment="edge" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Show code
<div data-slot="slider" data-default-value="25,75" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-default-value="25,75" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Disabled state
data-disabled keeps the current value visible but blocks dragging and keyboard changes.
Show code
<div data-slot="slider" data-disabled data-default-value="50" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-disabled data-default-value="50" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Show code
<div data-slot="slider" data-disabled data-default-value="25" data-thumb-alignment="edge" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-disabled data-default-value="25" data-thumb-alignment="edge" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Show code
<div data-slot="slider" data-disabled data-default-value="25,75" class="slider">
<div data-slot="slider-track" class="slider-track">
<div data-slot="slider-range" class="slider-range"></div>
</div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
<div data-slot="slider-thumb" class="slider-thumb"></div>
</div>
<style>
.slider {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 1.25rem;
}
.slider-track {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 0.25rem;
}
.slider-range {
background: var(--text);
border-radius: 0.25rem;
}
.slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: var(--surface);
border: 2px solid var(--text);
border-radius: 50%;
cursor: grab;
outline: none;
}
.slider-thumb:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 20%, transparent);
}
.slider-thumb[data-dragging] { cursor: grabbing; }
.slider[data-disabled] .slider-thumb {
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
</style><div data-slot="slider" data-disabled data-default-value="25,75" class="relative flex h-5 w-full items-center">
<div data-slot="slider-track" class="h-1 w-full rounded bg-[var(--border)]">
<div data-slot="slider-range" class="rounded bg-text"></div>
</div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
<div
data-slot="slider-thumb"
class="h-5 w-5 rounded-full border-2 border-text bg-[var(--surface)] cursor-grab focus:outline-none focus:ring-2
focus:ring-[var(--text)]/20 data-dragging:cursor-grabbing"
></div>
</div>import { create } from "@data-slot/slider";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());API reference
Initialization
create(scope?)
Find and bind uninitialized [data-slot="slider"] descendants of scope (defaults to document). Returns SliderController[] for newly bound roots. To initialize the scope element itself, use createSlider.
import { create } from "@data-slot/slider";
const controllers = create();
createSlider(root, options?)
Create a SliderController for one root element. JavaScript options take precedence over the corresponding data attributes. Calling this again for a bound root returns its existing controller; destroy it before rebinding with new options.
import { createSlider } from "@data-slot/slider";
const controller = createSlider(element, {});
Slots
Runtime Slots
slider- Root element that manages the value, bounds, orientation, and disabled state.slider-control- Optional interaction wrapper around the track and thumbs. When omitted, the track’s parent is used, typically the root.slider-track- Required track used to measure pointer position and lay out the range and thumbs.slider-range- Optional filled portion of the track; its position and size follow the selected value or range.slider-thumb- Required draggable control with slider semantics and keyboard support. Use one for a single value or two for a range; two thumbs enable range mode.
Data Attributes
| Attribute | Description | Default |
|---|---|---|
data-default-value |
Initial value (50 or 25,75 for range) |
min (single), [min, min] (range) |
data-min |
Minimum value | 0 |
data-max |
Maximum value | 100 |
data-step |
Step increment | 1 |
data-large-step |
Large step for PageUp/PageDown | step * 10 |
data-orientation |
horizontal or vertical |
horizontal |
data-thumb-alignment |
center, edge, or edge-client-only |
center |
data-disabled |
Disable the slider | - |
Options
JavaScript options take precedence over root data attributes.
| Option | Type | Default | Description |
|---|---|---|---|
defaultValue |
number | [number, number] |
min (single), [min, min] (range) |
Initial value; clamped to the bounds and snapped to step. Two thumbs enable range mode. |
min |
number |
0 |
Minimum value |
max |
number |
100 |
Maximum value |
step |
number |
1 |
Step increment; non-positive values fall back to 1 |
largeStep |
number |
step * 10 |
Increment for PageUp/PageDown and Shift+Arrow |
orientation |
"horizontal" | "vertical" |
"horizontal" |
Slider orientation |
thumbAlignment |
"center" | "edge" | "edge-client-only" |
"center" |
Thumb placement at the track edges |
disabled |
boolean |
false |
Disable user interaction and inbound set events |
onValueChange |
(value: number | [number, number]) => void |
undefined |
Called for value changes, including programmatic updates; silent on initialization and unchanged values |
onValueCommit |
(value: number | [number, number]) => void |
undefined |
Called on pointer release/cancel, blur after a keyboard value change, or a changed slider:set update |
Controller
| Method/Property | Description |
|---|---|
setValue(value: number | [number, number]) |
Update the value, including when disabled. Emits change when the value changes, but does not emit commit. |
value |
Current value (readonly number | [number, number]) |
min |
Resolved minimum (readonly number) |
max |
Resolved maximum (readonly number) |
disabled |
Whether interaction is disabled (readonly boolean) |
destroy() |
Remove listeners and release the root binding |
Events
Outbound Events
Listen for these events on the root element.
| Event | Detail | Description |
|---|---|---|
slider:change |
{ value: number | [number, number] } |
Fires during value changes |
slider:commit |
{ value: number | [number, number] } |
Fires on pointer release/cancel, blur after a keyboard value change, or a changed slider:set update |
Inbound Events
Dispatch these events on the root element.
| Event | Detail | Description |
|---|---|---|
slider:set |
{ value: number | [number, number] } |
Set value programmatically |
// Listen for changes
root.addEventListener("slider:change", (e) => {
console.log("Value:", e.detail.value);
});
// Set value from outside
root.dispatchEvent(new CustomEvent("slider:set", {
detail: { value: 50 }
}));
// Set range value
root.dispatchEvent(new CustomEvent("slider:set", {
detail: { value: [25, 75] }
}));
Note: Blocked when slider is disabled.
Deprecated Shapes
The following shapes are deprecated and will be removed in v1.0:
// Deprecated: bare number
root.dispatchEvent(new CustomEvent("slider:set", {
detail: 50
}));
// Deprecated: bare array
root.dispatchEvent(new CustomEvent("slider:set", {
detail: [25, 75]
}));
Use { value: ... } instead.
Styling
The component sets data attributes and inline styles for CSS hooks:
/* Root state */
[data-slot="slider"][data-orientation="horizontal"] { ... }
[data-slot="slider"][data-orientation="vertical"] { ... }
[data-slot="slider"][data-disabled] { ... }
[data-slot="slider"][data-dragging] { ... }
/* Parts mirror shared state for shadcn/Base-style selectors */
[data-slot="slider-track"][data-orientation="horizontal"] { ... }
[data-slot="slider-track"][data-orientation="vertical"] { ... }
[data-slot="slider-range"][data-orientation="horizontal"] { ... }
[data-slot="slider-range"][data-orientation="vertical"] { ... }
[data-slot="slider-track"][data-disabled] { ... }
[data-slot="slider-range"][data-disabled] { ... }
[data-slot="slider-thumb"][data-orientation="horizontal"] { ... }
[data-slot="slider-thumb"][data-orientation="vertical"] { ... }
[data-slot="slider-thumb"][data-disabled] { ... }
/* Thumb positioning (set automatically) */
[data-slot="slider-thumb"] {
position: absolute;
/* --position: X%; inset-inline-start/top or bottom/left are applied inline */
/* translate is applied inline to center the thumb on the resolved visual position */
}
/* Range thumbs expose their index */
[data-slot="slider-thumb"][data-index="0"] { ... }
[data-slot="slider-thumb"][data-index="1"] { ... }
/* Range positioning (set automatically) */
[data-slot="slider-range"] {
/* horizontal: relative + inset-inline-start/width */
/* vertical: absolute + bottom/height */
}
/* Thumb dragging state */
[data-slot="slider-thumb"][data-dragging] { ... }
slider-track, slider-range, and slider-thumb also get Base-style inline layout
styles from the controller. The runtime owns the track’s position: relative, the
range’s start/size positioning, and the thumb’s absolute position / translate, so
authors only need to supply visual styles unless they intentionally want to override
that layout behavior.
When thumbAlignment is "edge" or "edge-client-only", the runtime measures the
track and thumb size to keep the thumb inside the visible track. In those modes,
--position and the range start/size vars represent rendered layout positions rather
than raw value percentages.
Thumb Alignment
By default, slider thumbs are centered on the active value. That means the thumb can
extend beyond the track when the value is at min or max.
Use thumbAlignment or data-thumb-alignment to change that behavior:
"center"- Default. The thumb is centered on the value and may overflow the track edges."edge"- Insets the thumb so its edge aligns with the track edge atminandmax."edge-client-only"- Accepted for Base UI API parity. In this vanilla package it behaves the same as"edge".
<div data-slot="slider" data-default-value="25" data-thumb-alignment="edge">
<div data-slot="slider-track">
<div data-slot="slider-range"></div>
</div>
<div data-slot="slider-thumb"></div>
</div>
createSlider(element, {
defaultValue: 25,
thumbAlignment: "edge",
});
Keyboard Navigation
| Key | Action |
|---|---|
ArrowRight / ArrowLeft |
Increase / decrease by step in horizontal sliders |
ArrowUp / ArrowDown |
Increase / decrease by step in vertical sliders |
PageUp |
Increase by largeStep |
PageDown |
Decrease by largeStep |
Home |
Set to min |
End |
Set to max |
Shift+Arrow |
Move by largeStep |
Accessibility
Each thumb element receives:
role="slider"tabindex="0"aria-valuemin/aria-valuemax/aria-valuenowaria-orientationaria-disabled(when disabled)aria-label(fromdata-labelor auto-generated for range)
slider-track, slider-range, and slider-thumb also mirror the root’s
data-orientation and data-disabled attrs for styling. slider-control is optional;
when omitted, the root acts as the interactive control surface.