Skip to content
Dethink Components

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.

Usage
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.

72
48
64

Voice 72 · Music 48 · Ambience 64

18°C – 26°C

Set a lower and upper temperature.

Balanced

Named presets keep the choice simple.

Show sourceexamples/slider/vertical.tsx
examples/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

Steady

Five stops. A rhythm for every moment.

Preview paused — adjust freely, then press Play.

Show sourceexamples/slider/motion-speed.tsx
examples/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.

Steady

Meaningful choices, precise values underneath.

Slow
Slow – Fast
Show sourceexamples/slider/stepper.tsx
examples/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.

US$1,200 – US$3,600

Explore plans between $1,200 and $3,600 per month.

Show sourceexamples/slider/budget.tsx
examples/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

1.25

Adjust in quarter-speed increments. Release to apply.

Applied speed 1.25×

Show sourceexamples/slider/precision.tsx
examples/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.

25
50
75
65
35
60
Show sourceexamples/slider/states.tsx
examples/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";
Slider API
PropWhat it doesDefault
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" / —
localestringNumber 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) => voidReceive values during interaction or when an adjustment completes.Not set
min / max / stepnumberNumeric bounds and increment size.0 / 100 / 1
label / descriptionReactNodeVisible 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.NumberFormatOptionsDisable interaction or format numeric output and announcements.false / —
className / classNamesstring / SliderSlotsCustomize 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.