Indeterminate Progress
Original · freeindeterminate progress bar
byWeb InnoventixReact + Tailwind
progindeterminateprogress
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-indeterminate.jsonprog-indeterminate.tsx
"use client";
import { useEffect, useState, type CSSProperties } from "react";
import { useReducedMotion } from "motion/react";
type Variant = "flow" | "stripe" | "bounce";
const STYLES: { id: Variant; label: string }[] = [
{ id: "flow", label: "Flow" },
{ id: "stripe", label: "Stripes" },
{ id: "bounce", label: "Bounce" },
];
const SPEEDS = [0.5, 0.75, 1, 1.5, 2] as const;
type Task = {
id: string;
name: string;
meta: string;
gradient: string;
};
const TASKS: Task[] = [
{
id: "compile",
name: "Compiling TypeScript",
meta: "apps/web · 412 modules",
gradient: "from-indigo-500 to-violet-500",
},
{
id: "images",
name: "Optimizing images",
meta: "public/media · 88 assets",
gradient: "from-emerald-500 to-teal-500",
},
{
id: "upload",
name: "Uploading artifacts",
meta: "edge-cdn · us-east-1",
gradient: "from-amber-500 to-orange-500",
},
];
function PlayIcon() {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
<path d="M6.5 4.3a1 1 0 0 1 1.53-.85l7.2 4.7a1 1 0 0 1 0 1.7l-7.2 4.7a1 1 0 0 1-1.53-.85V4.3Z" />
</svg>
);
}
function PauseIcon() {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
<path d="M6 3.5A1.5 1.5 0 0 1 7.5 5v10a1.5 1.5 0 0 1-3 0V5A1.5 1.5 0 0 1 6 3.5Zm8 0A1.5 1.5 0 0 1 15.5 5v10a1.5 1.5 0 0 1-3 0V5A1.5 1.5 0 0 1 14 3.5Z" />
</svg>
);
}
function ResetIcon() {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4 fill-current">
<path d="M10 3.5a6.5 6.5 0 1 0 6.32 8.03 1 1 0 1 0-1.94-.47A4.5 4.5 0 1 1 10 5.5c1.05 0 2.02.36 2.79.96l-1.5 1.5a.6.6 0 0 0 .43 1.03H16a.6.6 0 0 0 .6-.6V4.13a.6.6 0 0 0-1.03-.42l-1.16 1.15A6.47 6.47 0 0 0 10 3.5Z" />
</svg>
);
}
const STRIPE_IMAGE =
"repeating-linear-gradient(45deg, rgba(255,255,255,0.28) 0, rgba(255,255,255,0.28) 10px, rgba(255,255,255,0) 10px, rgba(255,255,255,0) 20px)";
function IndeterminateBar({
variant,
running,
reduce,
gradient,
label,
trackClass,
restartKey,
}: {
variant: Variant;
running: boolean;
reduce: boolean;
gradient: string;
label: string;
trackClass: string;
restartKey: number;
}) {
const play: CSSProperties = { animationPlayState: running ? "running" : "paused" };
return (
<div
role="progressbar"
aria-label={label}
aria-busy={running}
aria-valuetext={running ? "In progress, time remaining unknown" : "Paused"}
className={`relative w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800 ${trackClass}`}
>
{reduce ? (
<>
<span className={`absolute inset-0 bg-gradient-to-r opacity-80 ${gradient}`} />
<span className="absolute inset-0" style={{ backgroundImage: STRIPE_IMAGE }} />
</>
) : variant === "flow" ? (
<div key={restartKey} className="absolute inset-0">
<span
className={`progind-flow-a absolute top-0 bottom-0 rounded-full bg-gradient-to-r ${gradient}`}
style={play}
/>
<span
className={`progind-flow-b absolute top-0 bottom-0 rounded-full bg-gradient-to-r ${gradient}`}
style={play}
/>
</div>
) : variant === "stripe" ? (
<div key={restartKey} className="absolute inset-0">
<span className={`absolute inset-0 bg-gradient-to-r ${gradient}`} />
<span
className="progind-stripe-fill absolute inset-0"
style={{ ...play, backgroundImage: STRIPE_IMAGE }}
/>
</div>
) : (
<div key={restartKey} className="absolute inset-0">
<span
className={`progind-bounce-chunk absolute top-0 bottom-0 w-[28%] rounded-full bg-gradient-to-r ${gradient}`}
style={play}
/>
</div>
)}
</div>
);
}
export default function ProgIndeterminate() {
const prefersReduced = useReducedMotion();
const reduce = prefersReduced === true;
const [running, setRunning] = useState(true);
const [variant, setVariant] = useState<Variant>("flow");
const [speed, setSpeed] = useState(1);
const [elapsed, setElapsed] = useState(0);
const [restartKey, setRestartKey] = useState(0);
useEffect(() => {
if (!running) return;
const id = window.setInterval(() => setElapsed((e) => e + 1), 1000);
return () => window.clearInterval(id);
}, [running]);
const duration = (2.1 / speed).toFixed(2) + "s";
const shellStyle = { "--pd": duration } as CSSProperties;
const mm = String(Math.floor(elapsed / 60)).padStart(2, "0");
const ss = String(elapsed % 60).padStart(2, "0");
function reset() {
setElapsed(0);
setRunning(true);
setRestartKey((k) => k + 1);
}
return (
<section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-24">
<style>{`
@keyframes progind-bar1 {
0% { left: -35%; right: 100%; }
60% { left: 100%; right: -90%; }
100% { left: 100%; right: -90%; }
}
@keyframes progind-bar2 {
0% { left: -200%; right: 100%; }
60% { left: 107%; right: -8%; }
100% { left: 107%; right: -8%; }
}
@keyframes progind-stripe-move {
from { background-position: 0 0; }
to { background-position: 28.28px 0; }
}
@keyframes progind-bounce-move {
from { transform: translateX(0); }
to { transform: translateX(257%); }
}
.progind-flow-a {
animation: progind-bar1 var(--pd, 2.1s) cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
will-change: left, right;
}
.progind-flow-b {
animation: progind-bar2 var(--pd, 2.1s) cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation-delay: calc(var(--pd, 2.1s) * -0.6);
will-change: left, right;
}
.progind-stripe-fill {
animation: progind-stripe-move var(--pd, 2.1s) linear infinite;
will-change: background-position;
}
.progind-bounce-chunk {
animation: progind-bounce-move var(--pd, 2.1s) cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
.progind-flow-a, .progind-flow-b, .progind-stripe-fill, .progind-bounce-chunk {
animation: none !important;
}
}
`}</style>
<div
style={shellStyle}
className="mx-auto flex max-w-2xl flex-col gap-6"
>
<header className="flex flex-col gap-3">
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-600 dark:border-slate-800 dark:bg-slate-900 dark:text-indigo-400">
<span
className={`h-2 w-2 rounded-full ${
running ? "bg-emerald-500" : "bg-slate-400 dark:bg-slate-600"
} ${running && !reduce ? "animate-pulse" : ""}`}
/>
Pipeline status
</span>
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
Deploying to production
</h2>
<p className="max-w-prose text-sm text-slate-600 dark:text-slate-400">
This step doesn't report a percentage — we can't know how long it
takes. The bar loops to signal work is happening until the build resolves.
</p>
</header>
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
<div className="mb-3 flex items-center justify-between text-sm">
<span className="font-medium text-slate-700 dark:text-slate-200">
{running ? "Building bundle…" : "Paused"}
</span>
<span className="tabular-nums font-medium text-slate-500 dark:text-slate-400">
Elapsed {mm}:{ss}
</span>
</div>
<IndeterminateBar
variant={variant}
running={running}
reduce={reduce}
gradient="from-indigo-500 via-violet-500 to-sky-500"
label="Production build progress"
trackClass="h-3"
restartKey={restartKey}
/>
<p aria-live="polite" className="sr-only">
{running ? "Build in progress." : "Build paused."}
</p>
<div className="mt-5 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<button
type="button"
aria-pressed={running}
onClick={() => setRunning((r) => !r)}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 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-slate-900"
>
{running ? <PauseIcon /> : <PlayIcon />}
{running ? "Pause" : "Resume"}
</button>
<button
type="button"
onClick={reset}
className="inline-flex items-center gap-2 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-50 focus-visible: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-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
>
<ResetIcon />
Reset
</button>
</div>
<fieldset className="flex items-center gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800">
<legend className="sr-only">Animation style</legend>
{STYLES.map((s) => (
<label key={s.id} className="relative flex-1 cursor-pointer">
<input
type="radio"
name="progind-style"
value={s.id}
checked={variant === s.id}
onChange={() => setVariant(s.id)}
className="peer sr-only"
/>
<span className="flex select-none items-center justify-center rounded-lg px-3.5 py-1.5 text-sm font-medium text-slate-600 ring-1 ring-inset ring-transparent transition-colors peer-checked:bg-white peer-checked:text-indigo-600 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 dark:text-slate-300 dark:peer-checked:bg-slate-950 dark:peer-checked:text-indigo-400">
{s.label}
</span>
</label>
))}
</fieldset>
</div>
<div className="mt-4 flex items-center gap-3">
<label
htmlFor="progind-speed"
className="text-sm font-medium text-slate-600 dark:text-slate-400"
>
Speed
</label>
<input
id="progind-speed"
type="range"
min={0}
max={SPEEDS.length - 1}
step={1}
value={SPEEDS.indexOf(speed as (typeof SPEEDS)[number])}
onChange={(e) => setSpeed(SPEEDS[Number(e.target.value)])}
aria-valuetext={`${speed} times`}
className="h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
/>
<span className="w-10 text-right text-sm font-semibold tabular-nums text-slate-700 dark:text-slate-200">
{speed}×
</span>
</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
<h3 className="mb-4 text-sm font-semibold text-slate-700 dark:text-slate-200">
Concurrent steps
</h3>
<ul className="flex flex-col gap-4">
{TASKS.map((t) => (
<li key={t.id} className="flex flex-col gap-2">
<div className="flex items-baseline justify-between gap-3">
<span className="text-sm font-medium text-slate-800 dark:text-slate-100">
{t.name}
</span>
<span className="shrink-0 text-xs text-slate-500 dark:text-slate-400">
{t.meta}
</span>
</div>
<IndeterminateBar
variant="flow"
running={running}
reduce={reduce}
gradient={t.gradient}
label={`${t.name} progress`}
trackClass="h-1.5"
restartKey={restartKey}
/>
</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 Basic Progress
Originalbasic progress bars in colours

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

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.

