Vertical Slider
Original · freevertical sliders
byWeb InnoventixReact + Tailwind
sldverticalsliders
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/sld-vertical.jsonsld-vertical.tsx
"use client";
import {
useCallback,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
} from "react";
import { motion, useReducedMotion } from "motion/react";
const clamp = (n: number, lo: number, hi: number): number =>
Math.min(hi, Math.max(lo, n));
type Band = { label: string; freq: string };
const BANDS: Band[] = [
{ label: "Sub", freq: "60 Hz" },
{ label: "Bass", freq: "150 Hz" },
{ label: "Low Mid", freq: "400 Hz" },
{ label: "Mid", freq: "1 kHz" },
{ label: "Hi Mid", freq: "2.4 kHz" },
{ label: "Presence", freq: "6 kHz" },
{ label: "Air", freq: "12 kHz" },
];
type Preset = { name: string; values: number[] };
const FLAT: number[] = [0, 0, 0, 0, 0, 0, 0];
const PRESETS: Preset[] = [
{ name: "Flat", values: FLAT },
{ name: "Bass Boost", values: [8, 6, 3, 0, -1, -1, 0] },
{ name: "Vocal Clarity", values: [-3, -2, 0, 3, 5, 3, 1] },
{ name: "Live Rock", values: [5, 3, -1, 2, 4, 5, 3] },
{ name: "Acoustic", values: [3, 2, 1, 1, 2, 3, 4] },
];
const EQ_MIN = -12;
const EQ_MAX = 12;
const DEFAULT_MASTER = 72;
const eqLabel = (v: number): string => `${v > 0 ? "+" : ""}${v} dB`;
const pctLabel = (v: number): string => `${v}%`;
type FaderProps = {
label: string;
freq: string;
value: number;
min: number;
max: number;
step: number;
bigStep: number;
bipolar: boolean;
accentUp: string;
accentDown: string;
format: (v: number) => string;
onChange: (v: number) => void;
reduce: boolean;
};
function Fader({
label,
freq,
value,
min,
max,
step,
bigStep,
bipolar,
accentUp,
accentDown,
format,
onChange,
reduce,
}: FaderProps) {
const trackRef = useRef<HTMLDivElement | null>(null);
const draggingRef = useRef(false);
const [dragging, setDragging] = useState(false);
const pct = ((value - min) / (max - min)) * 100;
const centerPct = bipolar ? ((0 - min) / (max - min)) * 100 : 0;
const commit = useCallback(
(clientY: number) => {
const el = trackRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const ratio = clamp(1 - (clientY - rect.top) / rect.height, 0, 1);
const raw = min + ratio * (max - min);
const stepped = clamp(Math.round(raw / step) * step, min, max);
onChange(Number(stepped.toFixed(2)));
},
[min, max, step, onChange],
);
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
e.preventDefault();
trackRef.current?.setPointerCapture(e.pointerId);
draggingRef.current = true;
setDragging(true);
commit(e.clientY);
};
const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!draggingRef.current) return;
commit(e.clientY);
};
const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!draggingRef.current) return;
draggingRef.current = false;
setDragging(false);
if (trackRef.current?.hasPointerCapture(e.pointerId)) {
trackRef.current.releasePointerCapture(e.pointerId);
}
};
const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
let next = value;
switch (e.key) {
case "ArrowUp":
case "ArrowRight":
next = value + step;
break;
case "ArrowDown":
case "ArrowLeft":
next = value - step;
break;
case "PageUp":
next = value + bigStep;
break;
case "PageDown":
next = value - bigStep;
break;
case "Home":
next = min;
break;
case "End":
next = max;
break;
default:
return;
}
e.preventDefault();
onChange(clamp(Number(next.toFixed(2)), min, max));
};
const move =
dragging || reduce
? "transition-none"
: "transition-[bottom,height] duration-200 ease-out";
const boost = value >= 0;
const fillColor = bipolar ? (boost ? accentUp : accentDown) : accentUp;
const fillStyle = bipolar
? boost
? { bottom: `${centerPct}%`, height: `${pct - centerPct}%` }
: { bottom: `${pct}%`, height: `${centerPct - pct}%` }
: { bottom: "0%", height: `${pct}%` };
return (
<div className="flex flex-col items-center gap-3">
<div className="w-14 rounded-md bg-slate-100 py-1 text-center font-mono text-xs tabular-nums text-slate-700 dark:bg-zinc-800/80 dark:text-zinc-200">
{format(value)}
</div>
<div
ref={trackRef}
role="slider"
tabIndex={0}
aria-label={`${label} band, ${freq}`}
aria-orientation="vertical"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-valuetext={format(value)}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={endDrag}
onPointerCancel={endDrag}
onKeyDown={onKeyDown}
className="relative h-56 w-12 cursor-grab touch-none select-none rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:cursor-grabbing dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
>
<div className="absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-slate-200 dark:bg-zinc-800" />
{bipolar ? (
<div
className="absolute left-1/2 h-px w-6 -translate-x-1/2 bg-slate-300 dark:bg-zinc-700"
style={{ bottom: `${centerPct}%` }}
/>
) : null}
<div
className={`absolute left-1/2 w-2 -translate-x-1/2 rounded-full ${fillColor} ${move}`}
style={fillStyle}
/>
<div
className={`absolute z-10 flex h-6 w-10 items-center justify-center gap-[3px] rounded-md border border-slate-300 bg-white shadow-md ${move} dark:border-zinc-600 dark:bg-zinc-800`}
style={{ bottom: `${pct}%`, left: "50%", transform: "translate(-50%,50%)" }}
>
<span className="h-2.5 w-px bg-slate-300 dark:bg-zinc-600" />
<span className="h-2.5 w-px bg-slate-300 dark:bg-zinc-600" />
<span className="h-2.5 w-px bg-slate-300 dark:bg-zinc-600" />
</div>
</div>
<div className="flex flex-col items-center leading-tight">
<span className="text-xs font-semibold text-slate-700 dark:text-zinc-200">
{label}
</span>
<span className="font-mono text-[10px] text-slate-400 dark:text-zinc-500">
{freq}
</span>
</div>
</div>
);
}
export default function VerticalFaderConsole() {
const reduce = Boolean(useReducedMotion());
const [bands, setBands] = useState<number[]>(FLAT);
const [master, setMaster] = useState<number>(DEFAULT_MASTER);
const setBand = useCallback((index: number, v: number) => {
setBands((prev) => prev.map((x, i) => (i === index ? v : x)));
}, []);
const applyPreset = useCallback((values: number[]) => {
setBands([...values]);
}, []);
const resetAll = useCallback(() => {
setBands([...FLAT]);
setMaster(DEFAULT_MASTER);
}, []);
const activePreset = useMemo<string | null>(
() =>
PRESETS.find((p) => p.values.every((v, i) => v === bands[i]))?.name ??
null,
[bands],
);
const { peak, level, status } = useMemo(() => {
const pk = Math.max(...bands);
const lvl = clamp((master / 100) * (1 + Math.max(pk, 0) / 18), 0, 1.12);
const st =
lvl >= 1
? {
label: "Clipping",
bar: "bg-rose-500 dark:bg-rose-400",
text: "text-rose-600 dark:text-rose-400",
}
: lvl >= 0.85
? {
label: "Hot",
bar: "bg-amber-500 dark:bg-amber-400",
text: "text-amber-600 dark:text-amber-400",
}
: {
label: "Nominal",
bar: "bg-emerald-500 dark:bg-emerald-400",
text: "text-emerald-600 dark:text-emerald-400",
};
return { peak: pk, level: lvl, status: st };
}, [bands, master]);
const eqCss = `
@keyframes sldv-eq { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }
@keyframes sldv-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.8); } }
.sldv-eq-bar { animation: sldv-eq 900ms ease-in-out infinite; transform-origin: bottom center; }
.sldv-dot { animation: sldv-pulse 1.7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.sldv-eq-bar, .sldv-dot { animation: none !important; transform: none !important; }
}
`;
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:px-6 sm:py-24 dark:bg-zinc-950">
<style>{eqCss}</style>
<div
aria-hidden
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-200/40 blur-3xl dark:bg-indigo-500/10"
/>
<motion.div
initial={reduce ? false : { opacity: 0, y: 24 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="relative mx-auto max-w-5xl"
>
<header className="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 dark:border-zinc-800 dark:bg-zinc-900">
<span className="sldv-dot h-2 w-2 rounded-full bg-emerald-500 dark:bg-emerald-400" />
<span className="text-[11px] font-semibold uppercase tracking-widest text-slate-500 dark:text-zinc-400">
Studio Rack · 7-Band EQ
</span>
<span
aria-hidden
className="ml-1 flex h-4 items-end gap-[3px]"
>
<span className="sldv-eq-bar h-full w-1 rounded-sm bg-indigo-500 dark:bg-indigo-400" style={{ animationDelay: "0ms" }} />
<span className="sldv-eq-bar h-full w-1 rounded-sm bg-violet-500 dark:bg-violet-400" style={{ animationDelay: "180ms" }} />
<span className="sldv-eq-bar h-full w-1 rounded-sm bg-indigo-500 dark:bg-indigo-400" style={{ animationDelay: "360ms" }} />
<span className="sldv-eq-bar h-full w-1 rounded-sm bg-violet-500 dark:bg-violet-400" style={{ animationDelay: "90ms" }} />
</span>
</div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Shape the mix, band by band.
</h2>
<p className="mt-2 max-w-md text-sm text-slate-600 dark:text-zinc-400">
Drag any fader, or focus it and nudge a decibel at a time with the
arrow keys. Presets glide the whole bank into place.
</p>
</div>
<button
type="button"
onClick={resetAll}
className="inline-flex items-center gap-2 self-start rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 sm:self-auto dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-offset-zinc-950"
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
<path d="M3 3v5h5" />
</svg>
Reset all
</button>
</header>
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-zinc-800 dark:bg-zinc-900">
<div className="mb-8 flex flex-wrap items-center gap-2">
<span className="mr-1 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
Presets
</span>
{PRESETS.map((p) => {
const isActive = activePreset === p.name;
return (
<button
key={p.name}
type="button"
aria-pressed={isActive}
onClick={() => applyPreset(p.values)}
className={
"rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900 " +
(isActive
? "border-indigo-500 bg-indigo-500 text-white dark:border-indigo-400 dark:bg-indigo-500"
: "border-slate-300 bg-white text-slate-600 hover:border-slate-400 hover:text-slate-900 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:text-white")
}
>
{p.name}
</button>
);
})}
</div>
<div className="overflow-x-auto pb-2">
<div className="flex min-w-max items-end justify-center gap-4 sm:gap-6">
{BANDS.map((b, i) => (
<Fader
key={b.freq}
label={b.label}
freq={b.freq}
value={bands[i]}
min={EQ_MIN}
max={EQ_MAX}
step={1}
bigStep={3}
bipolar
accentUp="bg-indigo-500 dark:bg-indigo-400"
accentDown="bg-rose-500 dark:bg-rose-400"
format={eqLabel}
onChange={(v) => setBand(i, v)}
reduce={reduce}
/>
))}
<div className="mx-1 h-56 w-px self-center bg-slate-200 dark:bg-zinc-800" />
<Fader
label="Master"
freq="Output"
value={master}
min={0}
max={100}
step={1}
bigStep={10}
bipolar={false}
accentUp="bg-emerald-500 dark:bg-emerald-400"
accentDown="bg-emerald-500 dark:bg-emerald-400"
format={pctLabel}
onChange={setMaster}
reduce={reduce}
/>
</div>
</div>
<div className="mt-8 flex flex-col gap-4 border-t border-slate-200 pt-6 sm:flex-row sm:items-center sm:justify-between dark:border-zinc-800">
<div className="flex-1">
<div className="mb-1.5 flex items-center justify-between text-xs">
<span className="font-semibold uppercase tracking-wider text-slate-400 dark:text-zinc-500">
Output level
</span>
<span className={`font-mono font-semibold ${status.text}`}>
{status.label}
</span>
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-zinc-800">
<div
className={`h-full rounded-full ${status.bar} ${reduce ? "" : "transition-[width] duration-300 ease-out"}`}
style={{ width: `${clamp(level * 100, 0, 100)}%` }}
/>
</div>
</div>
<dl className="flex shrink-0 items-center gap-6 font-mono text-sm">
<div className="text-right">
<dt className="text-[10px] uppercase tracking-wider text-slate-400 dark:text-zinc-500">
Master
</dt>
<dd className="font-semibold text-slate-900 tabular-nums dark:text-white">
{master}%
</dd>
</div>
<div className="text-right">
<dt className="text-[10px] uppercase tracking-wider text-slate-400 dark:text-zinc-500">
Peak band
</dt>
<dd className="font-semibold text-slate-900 tabular-nums dark:text-white">
{peak > 0 ? "+" : ""}
{peak} dB
</dd>
</div>
</dl>
</div>
</div>
</motion.div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Slider
Originalbasic range slider with value bubble

Range Dual Slider
Originaldual-handle min/max range

Steps Slider
Originalstepped slider with tick marks

Tooltip Slider
Originalslider with a floating value tooltip

Volume Slider
Originalvolume slider with icon

Price Range Slider
Originalprice range filter slider

Gradient Track Slider
Originalslider with a gradient track

Rating Slider
Originalsatisfaction rating slider

Brightness Slider
Originalbrightness/contrast control sliders

Marks Slider
Originalslider with labelled marks

Color Hue Slider
Originalhue/saturation color sliders

Progress Drag Slider
Originalmedia scrubber slider

