Bar Basic Progress
Original · freebasic progress bars in colours
byWeb InnoventixReact + Tailwind
progbarbasicprogress
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/prog-bar-basic.jsonprog-bar-basic.tsx
"use client";
import { useCallback, useId, useMemo, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type ColorKey = "indigo" | "violet" | "emerald" | "sky" | "amber" | "rose";
type Metric = {
id: string;
label: string;
hint: string;
color: ColorKey;
value: number;
};
type ColorStyle = {
fill: string;
text: string;
dot: string;
accent: string;
ring: string;
};
const COLORS: Record<ColorKey, ColorStyle> = {
indigo: {
fill: "bg-indigo-500 dark:bg-indigo-400",
text: "text-indigo-600 dark:text-indigo-300",
dot: "bg-indigo-500 dark:bg-indigo-400",
accent: "accent-indigo-600 dark:accent-indigo-400",
ring: "focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400",
},
violet: {
fill: "bg-violet-500 dark:bg-violet-400",
text: "text-violet-600 dark:text-violet-300",
dot: "bg-violet-500 dark:bg-violet-400",
accent: "accent-violet-600 dark:accent-violet-400",
ring: "focus-visible:ring-violet-500 dark:focus-visible:ring-violet-400",
},
emerald: {
fill: "bg-emerald-500 dark:bg-emerald-400",
text: "text-emerald-600 dark:text-emerald-300",
dot: "bg-emerald-500 dark:bg-emerald-400",
accent: "accent-emerald-600 dark:accent-emerald-400",
ring: "focus-visible:ring-emerald-500 dark:focus-visible:ring-emerald-400",
},
sky: {
fill: "bg-sky-500 dark:bg-sky-400",
text: "text-sky-600 dark:text-sky-300",
dot: "bg-sky-500 dark:bg-sky-400",
accent: "accent-sky-600 dark:accent-sky-400",
ring: "focus-visible:ring-sky-500 dark:focus-visible:ring-sky-400",
},
amber: {
fill: "bg-amber-500 dark:bg-amber-400",
text: "text-amber-600 dark:text-amber-300",
dot: "bg-amber-500 dark:bg-amber-400",
accent: "accent-amber-600 dark:accent-amber-400",
ring: "focus-visible:ring-amber-500 dark:focus-visible:ring-amber-400",
},
rose: {
fill: "bg-rose-500 dark:bg-rose-400",
text: "text-rose-600 dark:text-rose-300",
dot: "bg-rose-500 dark:bg-rose-400",
accent: "accent-rose-600 dark:accent-rose-400",
ring: "focus-visible:ring-rose-500 dark:focus-visible:ring-rose-400",
},
};
const INITIAL: Metric[] = [
{ id: "uploads", label: "Asset uploads", hint: "1,204 / 1,540 files synced", color: "indigo", value: 78 },
{ id: "training", label: "Model fine-tune", hint: "epoch 7 of 9", color: "violet", value: 64 },
{ id: "coverage", label: "Test coverage", hint: "statements + branches", color: "emerald", value: 92 },
{ id: "latency", label: "Latency budget", hint: "p95 within 220 ms target", color: "sky", value: 46 },
{ id: "storage", label: "Storage used", hint: "38.4 GB of 50 GB", color: "amber", value: 77 },
{ id: "errors", label: "Error budget spent", hint: "30-day rolling window", color: "rose", value: 23 },
];
function clamp(n: number): number {
if (Number.isNaN(n)) return 0;
return Math.min(100, Math.max(0, Math.round(n)));
}
export default function ProgBarBasic() {
const reduce = useReducedMotion();
const uid = useId();
const [metrics, setMetrics] = useState<Metric[]>(INITIAL);
const [striped, setStriped] = useState<boolean>(true);
const setValue = useCallback((id: string, next: number) => {
setMetrics((prev) =>
prev.map((m) => (m.id === id ? { ...m, value: clamp(next) } : m)),
);
}, []);
const randomize = useCallback(() => {
setMetrics((prev) => prev.map((m) => ({ ...m, value: clamp(5 + Math.random() * 95) })));
}, []);
const reset = useCallback(() => setMetrics(INITIAL), []);
const average = useMemo(() => {
if (metrics.length === 0) return 0;
const sum = metrics.reduce((acc, m) => acc + m.value, 0);
return Math.round(sum / metrics.length);
}, [metrics]);
const btn =
"inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950";
const stepBtn =
"inline-flex h-7 w-7 items-center justify-center rounded-md border border-slate-300 bg-white text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white dark:focus-visible:ring-offset-slate-950";
return (
<section className="relative w-full bg-white px-4 py-16 text-slate-900 sm:px-6 sm:py-24 lg:px-8 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes progbarbasic-move {
from { background-position: 0 0; }
to { background-position: 1rem 0; }
}
.progbarbasic-stripes {
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.28) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.28) 50%,
rgba(255, 255, 255, 0.28) 75%,
transparent 75%,
transparent
);
background-size: 1rem 1rem;
animation: progbarbasic-move 0.9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.progbarbasic-stripes { animation: none; }
}
`}</style>
<div className="mx-auto max-w-3xl">
<div className="flex flex-col gap-6 rounded-2xl border border-slate-200 bg-slate-50/60 p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/40">
{/* Header */}
<header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400">
Release 4.2 · staging
</p>
<h2 className="mt-1 text-2xl font-semibold tracking-tight sm:text-3xl">
Deployment readiness
</h2>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
Six live signals as basic colored progress bars. Drag a slider or use the steppers — every bar updates in place.
</p>
</div>
<div className="flex shrink-0 flex-col items-start gap-1 sm:items-end">
<span className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
Overall
</span>
<span className="text-3xl font-bold tabular-nums text-slate-900 dark:text-white">
{average}
<span className="ml-0.5 text-lg font-semibold text-slate-400 dark:text-slate-500">%</span>
</span>
</div>
</header>
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-3 border-y border-slate-200 py-4 dark:border-slate-800">
<button type="button" onClick={randomize} className={btn}>
<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="M16 3h5v5" />
<path d="M4 20 21 3" />
<path d="M21 16v5h-5" />
<path d="m15 15 6 6" />
<path d="M4 4l5 5" />
</svg>
Randomize
</button>
<button type="button" onClick={reset} className={btn}>
<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 12a9 9 0 1 0 3-6.7L3 8" />
<path d="M3 3v5h5" />
</svg>
Reset
</button>
<button
type="button"
role="switch"
aria-checked={striped}
onClick={() => setStriped((s) => !s)}
className={`ml-auto inline-flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm font-medium text-slate-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-200 dark:focus-visible:ring-offset-slate-950`}
>
<span
aria-hidden="true"
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
striped ? "bg-indigo-500 dark:bg-indigo-400" : "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
striped ? "translate-x-4" : "translate-x-0.5"
}`}
/>
</span>
Animated stripes
</button>
</div>
{/* Bars */}
<ul className="flex flex-col gap-7">
{metrics.map((m) => {
const c = COLORS[m.color];
const barId = `${uid}-${m.id}`;
return (
<li key={m.id}>
<div className="flex items-baseline justify-between gap-3">
<span className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-slate-100">
<span aria-hidden="true" className={`h-2.5 w-2.5 shrink-0 rounded-full ${c.dot}`} />
{m.label}
</span>
<span className={`text-sm font-bold tabular-nums ${c.text}`}>{m.value}%</span>
</div>
<p className="mt-0.5 pl-[1.125rem] text-xs text-slate-500 dark:text-slate-400">{m.hint}</p>
<div
role="progressbar"
aria-label={m.label}
aria-valuenow={m.value}
aria-valuemin={0}
aria-valuemax={100}
aria-valuetext={`${m.value} percent`}
className="relative mt-2 h-2.5 w-full overflow-hidden rounded-full bg-slate-200/90 dark:bg-slate-800"
>
<motion.div
className={`relative h-full rounded-full ${c.fill}`}
initial={false}
animate={{ width: `${m.value}%` }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 150, damping: 24 }}
>
{striped && (
<span aria-hidden="true" className="progbarbasic-stripes absolute inset-0 rounded-full" />
)}
</motion.div>
</div>
<div className="mt-3 flex items-center gap-3">
<label htmlFor={barId} className="sr-only">
Set {m.label}
</label>
<input
id={barId}
type="range"
min={0}
max={100}
step={1}
value={m.value}
onChange={(e) => setValue(m.id, Number(e.target.value))}
className={`h-1.5 w-full grow cursor-pointer appearance-none rounded-full bg-slate-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-800 dark:focus-visible:ring-offset-slate-950 ${c.accent} ${c.ring}`}
/>
<div className="flex shrink-0 items-center gap-1">
<button
type="button"
onClick={() => setValue(m.id, m.value - 5)}
disabled={m.value <= 0}
aria-label={`Decrease ${m.label} by 5 percent`}
className={stepBtn}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" className="h-3.5 w-3.5" aria-hidden="true">
<path d="M5 12h14" />
</svg>
</button>
<button
type="button"
onClick={() => setValue(m.id, m.value + 5)}
disabled={m.value >= 100}
aria-label={`Increase ${m.label} by 5 percent`}
className={stepBtn}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" className="h-3.5 w-3.5" aria-hidden="true">
<path d="M12 5v14M5 12h14" />
</svg>
</button>
</div>
</div>
</li>
);
})}
</ul>
</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 →
Bar Striped Progress
Originalanimated striped progress bar

Bar Gradient Progress
Originalgradient progress bar with label

Bar Steps Progress
Originalstepped progress bar

Ring Progress
Originalcircular progress ring with percent

Ring Gradient Progress
Originalgradient circular progress

Semicircle Progress
Originalsemicircle gauge progress

Multi Progress
Originalmulti-segment stacked progress

Indeterminate Progress
Originalindeterminate progress bar

Label Inside Progress
Originalprogress bar with inner label

Upload Progress
Originalfile upload progress rows

Skill Bars Progress
Originalanimated skill/percent bars

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

