Slider
An input where the user selects a value from within a given range.
Demo
Dependencies
clsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
export interface SliderProps {
value?: number[];
defaultValue?: number[];
onValueChange?: (value: number[]) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
className?: string;
}
export function Slider({
value: controlledValue,
defaultValue = [0],
onValueChange,
min = 0,
max = 100,
step = 1,
disabled,
className,
}: SliderProps) {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const trackRef = useRef<HTMLDivElement>(null);
const current = value[0] ?? min;
const pct = ((current - min) / (max - min)) * 100;
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
const track = trackRef.current;
if (!track || disabled) return;
const update = (clientX: number) => {
const rect = track.getBoundingClientRect();
const ratio = Math.min(
Math.max((clientX - rect.left) / rect.width, 0),
1,
);
const raw = min + ratio * (max - min);
const stepped = Math.round(raw / step) * step;
const clamped = Math.min(Math.max(stepped, min), max);
const next = [clamped];
if (!isControlled) setInternalValue(next);
onValueChange?.(next);
};
update(e.clientX);
const handleMouseMove = (ev: MouseEvent) => update(ev.clientX);
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[min, max, step, disabled, isControlled, onValueChange],
);
return (
<div
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={current}
tabIndex={disabled ? -1 : 0}
onKeyDown={(e) => {
const delta =
e.key === "ArrowRight" || e.key === "ArrowUp"
? step
: e.key === "ArrowLeft" || e.key === "ArrowDown"
? -step
: 0;
if (delta) {
e.preventDefault();
const next = Math.min(Math.max(current + delta, min), max);
if (!isControlled) setInternalValue([next]);
onValueChange?.([next]);
}
}}
className={cn(
"relative flex h-5 w-full touch-none items-center",
className,
)}
>
<div
ref={trackRef}
onMouseDown={handleMouseDown}
className="relative h-1.5 w-full rounded-full bg-gray-200"
>
<div
className="absolute h-full rounded-full bg-gray-900"
style={{ width: `${pct}%` }}
/>
<div
className="absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-gray-900 bg-white shadow-sm"
style={{ left: `${pct}%` }}
/>
</div>
</div>
);
}
CLI
npx genui add slider