ComponentsSlider
Slider
Choose a value, find a range, or snap to a meaningful milestone.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- React Aria
(react-aria)Accessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { Slider } from "@dethink/components";Drag, click the track, or use your keyboard. Every adjustment stays precise.
Vertical controls
A compact mixer, temperature range and named focus presets. Values increase upward in both reading directions.
Studio / 01
Find your balance
Three channels. One quieter workspace.
Voice 72 · Music 48 · Ambience 64
Show sourceexamples/slider/vertical.tsx
"use client";
import { useState } from "react";
import { Slider, ExpressiveSlider } from "@dethink/components";
export function SliderVertical() {
const [levels, setLevels] = useState([72, 48, 64]);
return (
<div className="w-full space-y-8">
<div className="border-border bg-background rounded-2xl border p-6">
<div className="mb-6 flex items-start justify-between gap-4">
<div>
<p className="text-muted-foreground text-xs font-medium tracking-widest uppercase">
Studio / 01
</p>
<h3 className="mt-2 text-lg font-semibold">Find your balance</h3>
<p className="text-muted-foreground mt-1 text-sm">
Three channels. One quieter workspace.
</p>
</div>
<button
type="button"
className="text-muted-foreground hover:text-foreground rounded px-2 py-1 text-xs underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-4"
onClick={() => setLevels([72, 48, 64])}
>
Reset mix
</button>
</div>
<div className="grid grid-cols-3 gap-3 sm:gap-8">
{["Voice", "Music", "Ambience"].map((label, index) => (
<Slider
key={label}
label={label}
orientation="vertical"
size="xl"
value={levels[index]}
onValueChange={(next) =>
setLevels((current) =>
current.map((value, i) => (i === index ? next : value)),
)
}
className="[--dt-slider-track-length:10rem] [&_[data-slot=slider-output]]:hidden"
/>
))}
</div>
<p className="text-muted-foreground mt-5 text-center text-xs tabular-nums">
Voice {levels[0]} · Music {levels[1]} · Ambience {levels[2]}
</p>
</div>
<div className="grid gap-8 sm:grid-cols-2">
<Slider<[number, number]>
label="Comfort zone"
orientation="vertical"
defaultValue={[18, 26]}
min={10}
max={35}
formatOptions={{ style: "unit", unit: "celsius" }}
description="Set a lower and upper temperature."
/>
<ExpressiveSlider
label="Focus level"
orientation="vertical"
mode="stepper"
defaultValue={1}
steps={[
{ value: 0, label: "Quiet" },
{ value: 1, label: "Balanced" },
{ value: 4, label: "Deep" },
]}
description="Named presets keep the choice simple."
/>
</div>
</div>
);
}Motion speed
An inset control with a little spring. Choose a pace and watch the orbit respond; reduced-motion preferences start the preview paused.
A little momentum
Find your rhythm
Steady
1.0× your pace
Preview paused — adjust freely, then press Play.
Show sourceexamples/slider/motion-speed.tsx
"use client";
import { useState, useSyncExternalStore } from "react";
import { Button, ExpressiveSlider } from "@dethink/components";
import {
motion,
useAnimationFrame,
useMotionValue,
useReducedMotion,
} from "motion/react";
import { Pause, Play } from "lucide-react";
import { speedSteps } from "./speed-steps";
const subscribe = () => () => {};
const client = () => true;
const server = () => false;
export function SliderMotionSpeed() {
const [speed, setSpeed] = useState(1);
const [pauseOverride, setPauseOverride] = useState<boolean | null>(null);
const hydrated = useSyncExternalStore(subscribe, client, server);
const reduced = useReducedMotion();
const paused = pauseOverride ?? (!hydrated || reduced !== false);
const rotation = useMotionValue(0);
useAnimationFrame((_, delta) => {
if (!paused && speed > 0)
rotation.set((rotation.get() + Math.min(delta, 50) * speed * 0.03) % 360);
});
const selected = speedSteps.find((step) => step.value === speed)!;
return (
<div className="border-border bg-background mx-auto w-full max-w-lg overflow-hidden rounded-3xl border shadow-sm">
<div className="flex items-center justify-between gap-4 px-6 pt-6">
<div>
<p className="text-muted-foreground text-[10px] font-medium tracking-[0.18em] uppercase">
A little momentum
</p>
<h3 className="mt-1 text-lg font-semibold tracking-tight">
Find your rhythm
</h3>
</div>
<Button
size="sm"
className="flex shrink-0 items-center gap-1.5 whitespace-nowrap"
leftIcon={
paused ? <Play aria-hidden="true" /> : <Pause aria-hidden="true" />
}
variant="outline"
onClick={() => setPauseOverride(!paused)}
aria-label={paused ? "Play motion preview" : "Pause motion preview"}
>
{paused ? "Play" : "Pause"}
</Button>
</div>
<div
className="relative isolate flex h-64 items-center justify-center overflow-hidden"
data-slot="motion-preview"
data-paused={paused}
data-speed={speed}
>
<div
aria-hidden="true"
className="bg-primary/10 absolute size-40 rounded-full blur-3xl"
/>
<div
aria-hidden="true"
className="border-primary/10 absolute size-56 rounded-full border border-dashed"
/>
<div
aria-hidden="true"
className="border-primary/20 absolute size-40 rounded-full border"
/>
<motion.div
aria-hidden="true"
data-slot="motion-orbit"
style={{ rotate: rotation }}
className="pointer-events-none absolute size-40 rounded-full"
>
<span className="border-background bg-primary absolute top-1/2 -left-2 size-4 -translate-y-1/2 rounded-full border-[3px] shadow-[0_0_18px_color-mix(in_srgb,var(--dt-color-primary)_45%,transparent)]" />
<span className="bg-primary/30 absolute top-1/2 -right-1 size-2 -translate-y-1/2 rounded-full" />
</motion.div>
<div className="relative text-center">
<p className="text-3xl font-semibold tracking-tight">
{selected.label}
</p>
<p className="text-muted-foreground mt-1 text-xs tabular-nums">
{speed === 0 ? "Room to breathe" : `${speed.toFixed(1)}× your pace`}
</p>
</div>
</div>
<div className="border-border bg-muted/20 border-t px-6 pt-6 pb-7">
<ExpressiveSlider
label="Motion speed"
mode="stepper"
steps={speedSteps}
value={speed}
onValueChange={setSpeed}
size="xl"
valueDisplay="inline"
description="Five stops. A rhythm for every moment."
/>
<p className="text-muted-foreground mt-4 text-[11px]">
{paused
? "Preview paused — adjust freely, then press Play."
: speed === 0
? "Stillness is a setting, too."
: "Drag slowly. Feel each milestone settle into place."}
</p>
</div>
</div>
);
}Labelled steps
Named milestones snap to evenly spaced stops. The app receives the original numeric values, including uneven increments.
Show sourceexamples/slider/stepper.tsx
"use client";
import { Slider } from "@dethink/components";
import { speedSteps } from "./speed-steps";
export function SliderStepper() {
return (
<div className="border-border bg-background mx-auto grid w-full max-w-md gap-8 rounded-2xl border p-6">
<Slider
label="Movement"
mode="stepper"
steps={speedSteps}
defaultValue={1}
description="Meaningful choices, precise values underneath."
/>
<Slider
label="Endpoint labels"
mode="stepper"
steps={speedSteps.map((step, index) => ({
...step,
showLabel: index === 0 || index === speedSteps.length - 1,
}))}
defaultValue={0.5}
/>
<Slider<[number, number]>
label="Allowed speed range"
mode="stepper"
steps={speedSteps}
defaultValue={[0.5, 2]}
thumbLabels={["Slowest", "Fastest"]}
/>
</div>
);
}Budget range
Two independently labelled thumbs keep minimum and maximum values in order.
Make room for what matters
Find your sweet spot.
Explore plans between $1,200 and $3,600 per month.
Show sourceexamples/slider/budget.tsx
"use client";
import { useState } from "react";
import { Slider } from "@dethink/components";
export function SliderBudget() {
const [budget, setBudget] = useState<[number, number]>([1200, 3600]);
return (
<div className="border-border bg-background mx-auto w-full max-w-md rounded-2xl border p-6 shadow-sm">
<p className="text-muted-foreground text-xs font-medium tracking-widest uppercase">
Make room for what matters
</p>
<h3 className="mt-2 mb-6 text-xl font-semibold tracking-tight">
Find your sweet spot.
</h3>
<Slider<[number, number]>
label="Monthly budget"
valueDisplay="floating"
min={0}
max={5000}
step={100}
value={budget}
onValueChange={setBudget}
name={["budgetMin", "budgetMax"]}
formatOptions={{
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}}
/>
<p className="text-muted-foreground mt-4 text-sm leading-relaxed">
Explore plans between{" "}
<strong className="text-foreground font-medium">
${budget[0].toLocaleString("en-US")}
</strong>{" "}
and{" "}
<strong className="text-foreground font-medium">
${budget[1].toLocaleString("en-US")}
</strong>{" "}
per month.
</p>
</div>
);
}Precision control
Quarter-step adjustments with separate change and commit feedback.
Fine tuning
Applied speed 1.25×
Show sourceexamples/slider/precision.tsx
"use client";
import { useState } from "react";
import { Slider } from "@dethink/components";
export function SliderPrecision() {
const [value, setValue] = useState(1.25);
const [committed, setCommitted] = useState(1.25);
return (
<div className="border-border bg-background mx-auto w-full max-w-md rounded-2xl border p-6 shadow-sm">
<p className="text-muted-foreground mb-5 text-xs font-medium tracking-widest uppercase">
Fine tuning
</p>
<Slider
label="Playback speed"
min={0.25}
max={3}
step={0.25}
value={value}
onValueChange={setValue}
onValueCommit={setCommitted}
formatOptions={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
description="Adjust in quarter-speed increments. Release to apply."
/>
<p className="border-border text-muted-foreground mt-5 border-t pt-4 text-xs">
Applied speed{" "}
<span className="text-foreground float-end font-medium tabular-nums">
{committed.toFixed(2)}×
</span>
</p>
</div>
);
}Sizes and states
Four sizes, including an XL inset thumb, disabled state, compact density, and right-to-left layout.
Show sourceexamples/slider/states.tsx
"use client";
import { DethinkProvider, Slider } from "@dethink/components";
export function SliderStates() {
return (
<div className="mx-auto grid w-full max-w-md gap-6">
<Slider label="Small" size="sm" defaultValue={25} />
<Slider label="Medium" defaultValue={50} />
<Slider label="Large" size="lg" defaultValue={75} />
<Slider label="Extra large · inset thumb" size="xl" defaultValue={65} />
<Slider label="Disabled" disabled defaultValue={35} />
<DethinkProvider density="compact" dir="rtl">
<Slider label="Compact · right to left" defaultValue={60} />
</DethinkProvider>
</div>
);
}Semantic primary, muted, border, background and ring tokens follow your theme. Customize track, fill, thumb, marks and output with classNames. XL keeps the thumb inside a pill-shaped rail, including at the endpoints. Floating outputs retain an inline summary so values remain readable on small screens.
The basic Slider supports a static expressive variant without Motion. Install the optional slider-expressive registry item and import ExpressiveSlider for thumb compression and milestone pulses. Only decoration springs; the thumb always follows your pointer directly. Reduced motion removes the animation.
Expressive Slider registry item
npx shadcn@latest add https://components.dethink.co.uk/r/slider-expressive.json
import { ExpressiveSlider } from "@dethink/components";| Prop | What it does | Default |
|---|---|---|
orientation"horizontal" | "vertical" | Vertical values increase bottom to top, including in RTL. Set --dt-slider-track-length on the root to customize vertical travel (default 12rem). | horizontal |
variant"default" | "expressive" | Static themed glow. Install slider-expressive and use ExpressiveSlider for optional thumb spring and milestone pulse feedback; its defaults are expressive and floating. | default |
mode / steps"numeric" | "stepper" / readonly SliderStep[] | Stepper mode requires at least two strictly increasing finite values with readable labels. showLabel hides individual visible labels without removing accessible value text. Numeric min/max/step cannot be combined with steps. | "numeric" / — |
localestring | Number formatting locale; keyboard and pointer direction follow inherited DOM direction independently. | Inherited locale |
value / defaultValuenumber | [number, number] | A scalar or an ordered two-thumb range. Use matching value shapes throughout the component lifetime. | min |
onValueChange / onValueCommit(value) => void | Receive values during interaction or when an adjustment completes. | Not set |
min / max / stepnumber | Numeric bounds and increment size. | 0 / 100 / 1 |
label / descriptionReactNode | Visible accessible label and helper text. aria-label and aria-labelledby are also supported. | Not set |
name / thumbLabelsstring | [string, string] / [string, string] | Form field names and distinct accessible names for range thumbs. | — / Minimum, Maximum |
size / valueDisplay"sm" | "md" | "lg" | "xl" / "inline" | "floating" | "none" | Visual sizing and output presentation. Floating mode retains a stable inline summary for ranges. | md / inline |
disabled / formatOptionsboolean / Intl.NumberFormatOptions | Disable interaction or format numeric output and announcements. | false / — |
className / classNamesstring / SliderSlots | Customize the root, track, fill, thumb, marks, or output with portable Tailwind classes. | Not set |
Give every slider a visible label or an accessible name. Tab focuses each thumb; arrow keys adjust values, and Home/End reach the available bounds. Range thumbs cannot cross. Touch and pointer targets remain at least 44 pixels even with the small visual size. Descriptions provide additional context.