Bar Striped Progress
Original · freeanimated striped progress bar
byWeb InnoventixReact + Tailwind
progbarstripedprogress
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-striped.jsonprog-bar-striped.tsx
"use client";
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Status = "idle" | "running" | "paused" | "done";
type Task = {
id: string;
label: string;
detail: string;
rate: number;
};
const TASKS: Task[] = [
{ id: "media", label: "Media library", detail: "2.4 GB · 1,208 files", rate: 0.9 },
{ id: "db", label: "Database snapshot", detail: "860 MB · 1 archive", rate: 1.5 },
{ id: "config", label: "Environment config", detail: "12 MB · 44 keys", rate: 2.6 },
{ id: "search", label: "Search index", detail: "540 MB · 6 shards", rate: 1.15 },
];
const SPEEDS = [0.5, 1, 2];
const TICK_MS = 120;
const AVG_RATE = TASKS.reduce((a, t) => a + t.rate, 0) / TASKS.length;
function taskState(status: Status, value: number): string {
if (value >= 100) return "Complete";
if (status === "idle") return "Queued";
if (status === "paused") return "Paused";
if (value <= 0) return "Queued";
return "Transferring";
}
export default function StripedProgressSync() {
const prefersReduced = useReducedMotion();
const [progress, setProgress] = useState<number[]>(() => TASKS.map(() => 0));
const [status, setStatus] = useState<Status>("idle");
const [speed, setSpeed] = useState<number>(1);
const [striped, setStriped] = useState<boolean>(true);
const speedRefs = useRef<Array<HTMLButtonElement | null>>([]);
useEffect(() => {
if (status !== "running") return;
const id = window.setInterval(() => {
setProgress((prev) =>
prev.map((v, i) => {
if (v >= 100) return 100;
const step = TASKS[i].rate * speed * (0.6 + Math.random() * 0.8);
return Math.min(100, v + step);
}),
);
}, TICK_MS);
return () => window.clearInterval(id);
}, [status, speed]);
useEffect(() => {
if (status === "running" && progress.every((v) => v >= 100)) {
setStatus("done");
}
}, [progress, status]);
const overall = progress.reduce((a, b) => a + b, 0) / progress.length;
const overallPct = Math.round(overall);
const animating = striped && status === "running";
const etaTicks = overall >= 100 ? 0 : (100 - overall) / (AVG_RATE * speed);
const etaSeconds = Math.max(0, Math.ceil((etaTicks * TICK_MS) / 1000));
const primaryLabel =
status === "running"
? "Pause"
: status === "paused"
? "Resume"
: status === "done"
? "Completed"
: "Start sync";
const statusMessage =
status === "idle"
? "Ready to sync"
: status === "running"
? "Sync in progress"
: status === "paused"
? "Sync paused"
: "Sync complete";
const togglePrimary = () => {
setStatus((s) => {
if (s === "running") return "paused";
if (s === "done") return "done";
return "running";
});
};
const reset = () => {
setProgress(TASKS.map(() => 0));
setStatus("idle");
};
const handleSpeedKey = (e: KeyboardEvent<HTMLDivElement>) => {
const idx = SPEEDS.indexOf(speed);
let ni = idx;
if (e.key === "ArrowRight" || e.key === "ArrowDown") ni = (idx + 1) % SPEEDS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
ni = (idx - 1 + SPEEDS.length) % SPEEDS.length;
else return;
e.preventDefault();
setSpeed(SPEEDS[ni]);
speedRefs.current[ni]?.focus();
};
const rise = (delay: number) =>
prefersReduced
? { initial: false as const }
: {
initial: { opacity: 0, y: 14 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, delay, ease: [0.22, 1, 0.36, 1] as const },
};
const btnBase =
"inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold 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-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-neutral-950 dark:text-slate-100">
<style>{`
.progbarstriped-stripes {
background-image: repeating-linear-gradient(
45deg,
rgba(255, 255, 255, 0.24) 0,
rgba(255, 255, 255, 0.24) 9px,
transparent 9px,
transparent 18px
);
background-size: 25.46px 25.46px;
}
.progbarstriped-stripes[data-animate="true"] {
animation: progbarstriped-slide 0.75s linear infinite;
}
@keyframes progbarstriped-slide {
from { background-position: 0 0; }
to { background-position: 25.46px 0; }
}
.progbarstriped-glow[data-animate="true"] {
animation: progbarstriped-pulse 2.4s ease-in-out infinite;
}
@keyframes progbarstriped-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.75; }
}
@media (prefers-reduced-motion: reduce) {
.progbarstriped-stripes[data-animate="true"],
.progbarstriped-glow[data-animate="true"] {
animation: none;
}
}
`}</style>
<span className="sr-only" aria-live="polite">
{statusMessage}
</span>
<motion.div
{...rise(0)}
className="mx-auto w-full max-w-2xl rounded-3xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-neutral-800 dark:bg-neutral-900"
>
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<span
className={
"inline-flex h-2.5 w-2.5 rounded-full " +
(status === "done"
? "bg-emerald-500"
: status === "running"
? "bg-indigo-500"
: status === "paused"
? "bg-amber-500"
: "bg-slate-400 dark:bg-neutral-600")
}
aria-hidden="true"
/>
<p className="text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
{statusMessage}
</p>
</div>
<h2 className="mt-2 text-xl font-bold tracking-tight sm:text-2xl">
Syncing workspace to edge
</h2>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
3.8 GB across {TASKS.length} tasks · region <span className="font-medium text-slate-700 dark:text-slate-300">fra-1</span>
</p>
</div>
<div className="text-right">
<div className="text-4xl font-bold tabular-nums tracking-tight sm:text-5xl">
{overallPct}
<span className="text-2xl text-slate-400 dark:text-slate-500">%</span>
</div>
<p className="mt-1 text-xs text-slate-500 tabular-nums dark:text-slate-400">
{status === "done"
? "Finished"
: status === "running"
? `~${etaSeconds}s remaining`
: status === "paused"
? "Paused"
: "Not started"}
</p>
</div>
</div>
<div className="mt-6">
<div
className="relative h-4 w-full overflow-hidden rounded-full bg-slate-200 sm:h-5 dark:bg-neutral-800"
role="progressbar"
aria-label="Overall sync progress"
aria-valuenow={overallPct}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={
"relative h-full rounded-full bg-gradient-to-r transition-[width] duration-200 ease-out " +
(status === "done"
? "from-emerald-500 to-emerald-400"
: "from-indigo-600 via-violet-500 to-indigo-500")
}
style={{ width: `${overall}%` }}
>
<div
className="progbarstriped-stripes absolute inset-0 rounded-full"
data-animate={animating ? "true" : "false"}
aria-hidden="true"
/>
<div
className="progbarstriped-glow absolute inset-y-0 right-0 w-8 rounded-full bg-white/50 blur-md"
data-animate={animating ? "true" : "false"}
aria-hidden="true"
/>
</div>
</div>
</div>
<ul className="mt-8 space-y-5">
{TASKS.map((task, i) => {
const value = progress[i];
const pct = Math.round(value);
const done = value >= 100;
const label = taskState(status, value);
return (
<motion.li key={task.id} {...rise(0.08 + i * 0.06)}>
<div className="flex items-baseline justify-between gap-4">
<div className="min-w-0">
<p className="truncate text-sm font-semibold">{task.label}</p>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
{task.detail}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<span
className={
"rounded-full px-2 py-0.5 text-[11px] font-medium " +
(done
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
: label === "Transferring"
? "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300"
: label === "Paused"
? "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300"
: "bg-slate-100 text-slate-600 dark:bg-neutral-800 dark:text-slate-400")
}
>
{label}
</span>
<span className="w-10 text-right text-sm font-semibold tabular-nums text-slate-600 dark:text-slate-300">
{pct}%
</span>
</div>
</div>
<div
className="relative mt-2 h-2.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-neutral-800"
role="progressbar"
aria-label={`${task.label} progress`}
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={
"relative h-full rounded-full bg-gradient-to-r transition-[width] duration-200 ease-out " +
(done
? "from-emerald-500 to-emerald-400"
: "from-indigo-500 to-violet-500")
}
style={{ width: `${value}%` }}
>
<div
className="progbarstriped-stripes absolute inset-0 rounded-full"
data-animate={striped && status === "running" && !done ? "true" : "false"}
aria-hidden="true"
/>
</div>
</div>
</motion.li>
);
})}
</ul>
<motion.div
{...rise(0.36)}
className="mt-8 flex flex-wrap items-center gap-3 border-t border-slate-200 pt-6 dark:border-neutral-800"
>
<button
type="button"
onClick={togglePrimary}
disabled={status === "done"}
aria-label={primaryLabel}
className={
btnBase +
" h-10 px-4 text-white " +
(status === "running"
? "bg-amber-600 hover:bg-amber-500"
: "bg-indigo-600 hover:bg-indigo-500")
}
>
{status === "running" ? (
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
) : (
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d="M8 5.14v13.72a1 1 0 0 0 1.53.85l10.79-6.86a1 1 0 0 0 0-1.7L9.53 4.29A1 1 0 0 0 8 5.14Z" />
</svg>
)}
{primaryLabel}
</button>
<button
type="button"
onClick={reset}
disabled={status === "idle"}
aria-label="Reset sync"
className={
btnBase +
" h-10 border border-slate-300 bg-white px-4 text-slate-700 hover:bg-slate-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-slate-200 dark:hover:bg-neutral-800"
}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
<path d="M3 3v5h5" />
</svg>
Reset
</button>
<div className="ml-auto flex items-center gap-4">
<div className="flex items-center gap-2">
<span
id="progbarstriped-speed-label"
className="text-xs font-medium text-slate-500 dark:text-slate-400"
>
Speed
</span>
<div
role="radiogroup"
aria-labelledby="progbarstriped-speed-label"
onKeyDown={handleSpeedKey}
className="flex rounded-xl border border-slate-300 bg-slate-100 p-0.5 dark:border-neutral-700 dark:bg-neutral-800"
>
{SPEEDS.map((s, i) => {
const active = speed === s;
return (
<button
key={s}
ref={(el) => {
speedRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={active}
tabIndex={active ? 0 : -1}
onClick={() => setSpeed(s)}
className={
"rounded-lg px-2.5 py-1 text-xs font-semibold tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-neutral-800 " +
(active
? "bg-white text-indigo-700 shadow-sm dark:bg-neutral-950 dark:text-indigo-300"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200")
}
>
{s}×
</button>
);
})}
</div>
</div>
<button
type="button"
role="switch"
aria-checked={striped}
onClick={() => setStriped((v) => !v)}
className={
"inline-flex items-center gap-2 rounded-full text-xs font-medium text-slate-500 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:text-slate-400 dark:focus-visible:ring-offset-neutral-900"
}
>
<span
className={
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors " +
(striped ? "bg-indigo-600" : "bg-slate-300 dark:bg-neutral-700")
}
aria-hidden="true"
>
<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>
Stripes
</button>
</div>
</motion.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 →
Bar Basic Progress
Originalbasic progress bars in colours

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.

