Slider
Range input styled to the tokens, with an optional value label and formatter.
import { Slider }from "@/components/ui"
Specification
Selects a numeric value from a continuous range, with an optional live value label and custom formatter.
Props (10)
Every prop this component accepts, its type, default, and how to use it.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Visible label shown above the track and used as the aria-label. |
| value* | number | — | Controlled current value. |
| onChange* | (value: number) => void | — | Called with the new value as the slider moves. |
| min | number | 0 | Minimum selectable value. |
| max | number | 100 | Maximum selectable value. |
| step | number | 1 | Increment between selectable values. |
| disabled | boolean | — | Disables the slider. |
| showValue | boolean | — | Displays the current value beside the label. |
| format | (value: number) => string | String(v) | Formats the displayed value (e.g. append a % or unit). |
| className | string | — | Extra classes merged onto the wrapper. |
Volume & threshold
60%
25
Full source
The complete component, read from
src/components/ui/slider.tsx.slider.tsx
"use client";
import { useId } from "react";
import { cn } from "@/lib/utils";
interface SliderProps {
label?: string;
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
showValue?: boolean;
format?: (value: number) => string;
className?: string;
}
export function Slider({
label,
value,
onChange,
min = 0,
max = 100,
step = 1,
disabled,
showValue,
format = (v) => String(v),
className,
}: SliderProps) {
const id = useId();
const pct = ((value - min) / (max - min)) * 100;
return (
<div className={cn("w-full", className)}>
{(label || showValue) && (
<div className="mb-1.5 flex items-center justify-between text-[13px]">
{label && <label htmlFor={id} className="font-medium text-text">{label}</label>}
{showValue && <span className="tabular-nums text-text-secondary">{format(value)}</span>}
</div>
)}
<input
id={id}
type="range"
min={min}
max={max}
step={step}
value={value}
disabled={disabled}
aria-label={label}
onChange={(e) => onChange(Number(e.target.value))}
className={cn("ui-slider w-full", disabled && "cursor-not-allowed opacity-50")}
style={{
background: `linear-gradient(to right, var(--color-accent) ${pct}%, var(--color-bg-active) ${pct}%)`,
}}
/>
</div>
);
}Dependencies
To use Slider you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ └─ slider.tsx src/lib/ └─ utils.ts
Dependency files
These supporting files aren’t components — copy each one into the matching path above.
src/lib/utils.ts
utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Merge class names with clsx semantics + Tailwind conflict resolution.
* Prefer this in all components.
*/
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
/**
* Concatenate class names, filtering out falsy values.
* @deprecated Prefer {@link cn} — it also resolves Tailwind class conflicts.
*/
export function cx(...args: (string | false | null | undefined)[]): string {
return args.filter(Boolean).join(" ");
}
/**
* Focus ring style helper.
* @deprecated Focus rings are now handled globally via the
* `:focus-visible` rule in `globals.css` and the `focus-visible:*`
* utilities. This remains only for backwards compatibility.
*/
export function ring(color: string = "var(--color-accent)") {
return { outlineColor: color };
}