Slider
Drag, click or use the keys. The value rides above the thumb while you drag.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/slider.jsonVolume62%
Monthly budget$4,500
components/examples/slider-demo.tsx
"use client";
import * as React from "react";
import { Slider } from "@/components/ui/slider";
export default function SliderDemo() {
const [volume, setVolume] = React.useState(62);
const [budget, setBudget] = React.useState(4500);
return (
<div className="w-full max-w-sm space-y-8 rounded-xl border border-border bg-card p-6">
<div>
<div className="mb-3 flex justify-between text-sm"><span>Volume</span><span className="tabular-nums text-muted-foreground">{volume}%</span></div>
<Slider aria-label="Volume" value={volume} onValueChange={setVolume} format={(n) => `${n}%`} />
</div>
<div>
<div className="mb-3 flex justify-between text-sm"><span>Monthly budget</span><span className="tabular-nums text-muted-foreground">${budget.toLocaleString("en-US")}</span></div>
<Slider aria-label="Monthly budget" value={budget} onValueChange={setBudget} min={500} max={10000} step={250} format={(n) => `$${n.toLocaleString("en-US")}`} />
</div>
</div>
);
}When to use
- Picking a value from a continuous range where approximate is fine: volume, budget, rollout percentage.
When not to use
- Exact values: use a number field or input.
- Small counts like seats: use a number field.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/slider.json1Install the dependencies
npm install motion2Copy the source into your project
components/ui/slider.tsx"use client"; import * as React from "react"; import { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; export interface SliderProps { value?: number; defaultValue?: number; onValueChange?: (value: number) => void; /** Fires once when the drag ends or a key is released. */ onValueCommit?: (value: number) => void; min?: number; max?: number; step?: number; /** Shows the value in a bubble above the thumb while dragging. */ format?: (value: number) => string; disabled?: boolean; className?: string; "aria-label"?: string; } export function Slider({ value, defaultValue = 50, onValueChange, onValueCommit, min = 0, max = 100, step = 1, format, disabled, className, ...aria }: SliderProps) { const [internal, setInternal] = React.useState(defaultValue); const current = value ?? internal; const [dragging, setDragging] = React.useState(false); const [hover, setHover] = React.useState(false); const trackRef = React.useRef<HTMLDivElement>(null); const latest = React.useRef(current); const set = (raw: number) => { const snapped = Math.round((Math.min(max, Math.max(min, raw)) - min) / step) * step + min; const next = Number(snapped.toFixed(6)); latest.current = next; if (value === undefined) setInternal(next); onValueChange?.(next); }; const fromPointer = (clientX: number) => { const box = trackRef.current?.getBoundingClientRect(); if (!box) return; set(min + ((clientX - box.left) / box.width) * (max - min)); }; const pct = ((current - min) / (max - min)) * 100; const onKeyDown = (e: React.KeyboardEvent) => { const big = (max - min) / 10; const map: Record<string, number> = { ArrowRight: current + step, ArrowUp: current + step, ArrowLeft: current - step, ArrowDown: current - step, PageUp: current + big, PageDown: current - big, Home: min, End: max, }; if (map[e.key] === undefined) return; e.preventDefault(); set(map[e.key]); }; return ( <div ref={trackRef} onPointerDown={(e) => { if (disabled || e.button !== 0) return; e.currentTarget.setPointerCapture(e.pointerId); setDragging(true); fromPointer(e.clientX); }} onPointerMove={(e) => dragging && fromPointer(e.clientX)} onPointerUp={() => { if (!dragging) return; setDragging(false); onValueCommit?.(latest.current); }} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} className={cn("relative flex h-6 w-full touch-none select-none items-center", disabled ? "opacity-40" : "cursor-pointer", className)} > <div className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted"> <div className="absolute inset-y-0 left-0 rounded-full bg-primary" style={{ width: `${pct}%` }} /> </div> <motion.div role="slider" tabIndex={disabled ? -1 : 0} aria-label={aria["aria-label"]} aria-valuemin={min} aria-valuemax={max} aria-valuenow={current} aria-valuetext={format?.(current)} aria-disabled={disabled || undefined} onKeyDown={onKeyDown} onKeyUp={() => onValueCommit?.(latest.current)} className="absolute top-1/2 grid size-5 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-border bg-background shadow-md outline-none focus-visible:ring-2 focus-visible:ring-ring" style={{ left: `${pct}%` }} animate={{ scale: dragging ? 1.2 : hover ? 1.08 : 1 }} transition={{ type: "spring", stiffness: 500, damping: 30 }} > <span className="size-1.5 rounded-full bg-primary" /> <AnimatePresence> {format && dragging && ( <motion.span initial={{ opacity: 0, y: 4, scale: 0.9 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 4, scale: 0.9 }} transition={{ duration: 0.15 }} className="absolute bottom-full mb-2 whitespace-nowrap rounded-md bg-primary px-2 py-1 font-mono text-[11px] text-primary-foreground tabular-nums" > {format(current)} </motion.span> )} </AnimatePresence> </motion.div> </div> ); }
Usage
Import it from @/components/ui/slider after installing.
example.tsx
import * as React from "react";
import { Slider } from "@/components/ui/slider";
export function Budget() {
const [budget, setBudget] = React.useState(4500);
return <Slider aria-label="Budget" value={budget} onValueChange={setBudget} min={500} max={10000} step={250} format={(n) => `$${n}`} />;
}API reference
Slider
Drag the thumb or click the track. A value bubble rides above the thumb while dragging.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue | number | 50 | Controlled or uncontrolled value. |
| onValueChange | (n: number) => void | – | Called while dragging. |
| onValueCommit | (n: number) => void | – | Called once when the drag ends or a key is released. |
| min / max / step | number | 0 / 100 / 1 | Range and snapping. |
| format | (n: number) => string | – | Shows the bubble and sets aria-valuetext. |
Keyboard interactions
| Keys | Action |
|---|---|
| ←or↓ | Decrease by step |
| →or↑ | Increase by step |
| Page UporPage Down | Change by 10% of the range |
| HomeorEnd | Min or max |
Accessibility
- The thumb is role="slider" with min, max, now and valuetext.
- Use onValueCommit for expensive work so screen-reader users don't trigger it on every step.
Motion
- The thumb grows on hover (1.08) and press (1.2).
- The bubble pops in above the thumb while dragging.
Responsive behavior
- Fills its container width; touch dragging uses pointer capture.
Performance
- Only the fill width and thumb position change while dragging.
Notes for AI
- Save to the server in onValueCommit, not onValueChange.
This page as Markdown, for assistants and coding agents:
/components/slider/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt