Segmented Slider
Original · freesegmented level slider
byWeb InnoventixReact + Tailwind
sldsegmentedsliders
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-segmented.jsonsld-segmented.tsx
"use client";
import {
useId,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Level = {
name: string;
short: string;
hr: string;
cue: string;
blurb: string;
seg: string;
text: string;
softBg: string;
softBorder: string;
ring: string;
glow: string;
chip: string;
};
const LEVELS: Level[] = [
{
name: "Recovery",
short: "Recovery",
hr: "50–60%",
cue: "Nose-breathing only",
blurb:
"Active rest. A pace you could hold while telling a story. This is where adaptation happens — resist the urge to push it.",
seg: "bg-emerald-400 dark:bg-emerald-500",
text: "text-emerald-600 dark:text-emerald-400",
softBg: "bg-emerald-50 dark:bg-emerald-500/10",
softBorder: "border-emerald-200 dark:border-emerald-500/30",
ring: "border-emerald-500",
glow: "bg-emerald-400/50 dark:bg-emerald-400/40",
chip: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
},
{
name: "Easy",
short: "Easy",
hr: "60–70%",
cue: "Full sentences",
blurb:
"Aerobic base. Breathing is present but unhurried and you could still hold a full sentence. Most weekly volume lives here.",
seg: "bg-sky-400 dark:bg-sky-500",
text: "text-sky-600 dark:text-sky-400",
softBg: "bg-sky-50 dark:bg-sky-500/10",
softBorder: "border-sky-200 dark:border-sky-500/30",
ring: "border-sky-500",
glow: "bg-sky-400/50 dark:bg-sky-400/40",
chip: "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
},
{
name: "Moderate",
short: "Moderate",
hr: "70–80%",
cue: "Short phrases",
blurb:
"Tempo. Comfortably hard — the sweet spot where breathing deepens but stays controlled. Sentences shorten to phrases.",
seg: "bg-indigo-400 dark:bg-indigo-500",
text: "text-indigo-600 dark:text-indigo-400",
softBg: "bg-indigo-50 dark:bg-indigo-500/10",
softBorder: "border-indigo-200 dark:border-indigo-500/30",
ring: "border-indigo-500",
glow: "bg-indigo-400/50 dark:bg-indigo-400/40",
chip: "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
},
{
name: "Hard",
short: "Hard",
hr: "80–90%",
cue: "Single words",
blurb:
"Threshold. Talking breaks down to single words between breaths. Sustainable for minutes, not hours — treat it with respect.",
seg: "bg-amber-400 dark:bg-amber-500",
text: "text-amber-600 dark:text-amber-400",
softBg: "bg-amber-50 dark:bg-amber-500/10",
softBorder: "border-amber-200 dark:border-amber-500/30",
ring: "border-amber-500",
glow: "bg-amber-400/50 dark:bg-amber-400/40",
chip: "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
},
{
name: "Max Effort",
short: "Max",
hr: "90–100%",
cue: "No talking",
blurb:
"All out. Unsustainable past 60–90 seconds and no words come out. Reserve it for intervals and the final push.",
seg: "bg-rose-400 dark:bg-rose-500",
text: "text-rose-600 dark:text-rose-400",
softBg: "bg-rose-50 dark:bg-rose-500/10",
softBorder: "border-rose-200 dark:border-rose-500/30",
ring: "border-rose-500",
glow: "bg-rose-400/50 dark:bg-rose-400/40",
chip: "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
},
];
export default function SldSegmented() {
const [value, setValue] = useState<number>(1);
const [dragging, setDragging] = useState<boolean>(false);
const trackRef = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
const labelId = useId();
const max = LEVELS.length - 1;
const active = LEVELS[value];
const thumbPct = ((value + 0.5) / LEVELS.length) * 100;
const valueText = `Zone ${value + 1} of ${LEVELS.length}: ${active.name}, ${active.hr} of max heart rate`;
function valueFromClientX(clientX: number): number {
const el = trackRef.current;
if (!el) return value;
const rect = el.getBoundingClientRect();
const ratio = rect.width === 0 ? 0 : (clientX - rect.left) / rect.width;
const idx = Math.floor(ratio * LEVELS.length);
return Math.min(max, Math.max(0, idx));
}
function handlePointerDown(e: ReactPointerEvent<HTMLDivElement>): void {
e.currentTarget.setPointerCapture(e.pointerId);
setDragging(true);
setValue(valueFromClientX(e.clientX));
}
function handlePointerMove(e: ReactPointerEvent<HTMLDivElement>): void {
if (!dragging) return;
setValue(valueFromClientX(e.clientX));
}
function handlePointerUp(e: ReactPointerEvent<HTMLDivElement>): void {
setDragging(false);
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId);
}
}
function handleKeyDown(e: ReactKeyboardEvent<HTMLDivElement>): void {
let next = value;
switch (e.key) {
case "ArrowRight":
case "ArrowUp":
next = Math.min(max, value + 1);
break;
case "ArrowLeft":
case "ArrowDown":
next = Math.max(0, value - 1);
break;
case "PageUp":
next = Math.min(max, value + 2);
break;
case "PageDown":
next = Math.max(0, value - 2);
break;
case "Home":
next = 0;
break;
case "End":
next = max;
break;
default:
return;
}
e.preventDefault();
setValue(next);
}
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 text-slate-900 dark:from-slate-950 dark:to-slate-900 dark:text-slate-100 sm:px-6 sm:py-24">
<style>{`
@keyframes sldseg-halo {
0%, 100% { transform: scale(1); opacity: 0.45; }
50% { transform: scale(1.55); opacity: 0.05; }
}
.sldseg-halo { animation: sldseg-halo 2.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.sldseg-halo { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-300/20 blur-3xl dark:bg-indigo-500/10"
/>
<div className="relative mx-auto w-full max-w-2xl">
<div className="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/70 sm:p-9">
{/* Header */}
<div className="flex items-center gap-2">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-900 text-white dark:bg-white dark:text-slate-900">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<path d="M3 12h4l2 5 4-14 2 9h6" />
</svg>
</span>
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">
Training load
</span>
</div>
<h2
id={labelId}
className="mt-4 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-3xl"
>
How hard are we going today?
</h2>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
Choose an effort zone. Your heart-rate target and pace cue update instantly.
</p>
{/* Active display */}
<div
className={`mt-7 rounded-2xl border p-5 transition-colors duration-300 ${active.softBg} ${active.softBorder}`}
>
<AnimatePresence mode="wait">
<motion.div
key={value}
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
transition={{ duration: reduce ? 0 : 0.26, ease: "easeOut" }}
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-baseline gap-2">
<span className={`text-2xl font-bold tracking-tight ${active.text}`}>
{active.name}
</span>
<span className="text-sm font-medium text-slate-500 dark:text-slate-400">
Zone {value + 1}
</span>
</div>
<div className="flex items-center gap-2">
<span
className={`rounded-full px-2.5 py-1 text-xs font-semibold ${active.chip}`}
>
{active.hr} max HR
</span>
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300">
{active.cue}
</span>
</div>
</div>
<p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{active.blurb}
</p>
</motion.div>
</AnimatePresence>
</div>
{/* Slider */}
<div
role="slider"
tabIndex={0}
aria-labelledby={labelId}
aria-valuemin={1}
aria-valuemax={LEVELS.length}
aria-valuenow={value + 1}
aria-valuetext={valueText}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onKeyDown={handleKeyDown}
className="group mt-8 cursor-pointer touch-none select-none rounded-2xl px-1 py-4 outline-none focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-4 focus-visible:ring-offset-white dark:focus-visible:ring-white dark:focus-visible:ring-offset-slate-900"
>
<div ref={trackRef} className="relative">
{/* Base + segmented fill */}
<div className="flex h-3.5 gap-1.5 rounded-full">
{LEVELS.map((lvl, i) => (
<div
key={lvl.name}
className={`h-full flex-1 rounded-full transition-colors duration-300 ${
i <= value ? lvl.seg : "bg-slate-200 dark:bg-slate-800"
}`}
/>
))}
</div>
{/* Thumb */}
<motion.div
className="pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2"
animate={{ left: `${thumbPct}%` }}
transition={
reduce ? { duration: 0 } : { type: "spring", stiffness: 480, damping: 30 }
}
>
<span
className={`absolute inset-0 -z-10 m-auto h-6 w-6 rounded-full ${active.glow} ${
dragging ? "" : "sldseg-halo"
}`}
/>
<span
className={`flex h-7 w-7 items-center justify-center rounded-full border-[3px] bg-white text-[11px] font-bold text-slate-800 shadow-md transition-transform duration-150 dark:bg-slate-950 dark:text-slate-100 ${active.ring} ${
dragging ? "scale-110" : "group-hover:scale-105"
}`}
>
{value + 1}
</span>
</motion.div>
</div>
{/* Zone ticks */}
<div className="mt-4 grid grid-cols-5 gap-1.5">
{LEVELS.map((lvl, i) => (
<button
key={lvl.name}
type="button"
tabIndex={-1}
aria-hidden="true"
onClick={() => setValue(i)}
onPointerDown={(e) => e.stopPropagation()}
className="flex flex-col items-center gap-1 text-center"
>
<span
className={`h-1.5 w-1.5 rounded-full transition-colors duration-200 ${
i === value ? lvl.seg : "bg-slate-300 dark:bg-slate-700"
}`}
/>
<span
className={`text-[11px] font-medium transition-colors duration-200 ${
i === value
? `${lvl.text} font-semibold`
: "text-slate-400 dark:text-slate-500"
}`}
>
{lvl.short}
</span>
</button>
))}
</div>
</div>
{/* Footer */}
<div className="mt-7 flex flex-col gap-3 border-t border-slate-200 pt-5 text-sm dark:border-slate-800 sm:flex-row sm:items-center sm:justify-between">
<p className="text-slate-500 dark:text-slate-400">
Drag the dial, tap a zone, or nudge with the{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
←
</kbd>{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
→
</kbd>{" "}
keys.
</p>
<span className="shrink-0 font-medium text-slate-700 dark:text-slate-200">
Zone {value + 1} / {LEVELS.length}
</span>
</div>
</div>
</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

Vertical Slider
Originalvertical sliders

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

