Label Inside Progress
Original · freeprogress bar with inner label
byWeb InnoventixReact + Tailwind
proglabelinsideprogress
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-label-inside.jsonprog-label-inside.tsx
"use client";
import {
useEffect,
useRef,
useState,
type ChangeEvent,
type KeyboardEvent,
} from "react";
import { motion, useReducedMotion } from "motion/react";
type Status = "idle" | "running" | "paused" | "done";
type Speed = 0.5 | 1 | 2;
type Tone = "indigo" | "emerald" | "sky";
const SPEEDS: Speed[] = [0.5, 1, 2];
const TOTAL_FRAMES = 5400;
const TOTAL_MB = 214.6;
function phaseName(pct: number): string {
if (pct >= 100) return "Export complete";
if (pct >= 80) return "Finalizing MP4";
if (pct >= 55) return "Mixing audio";
if (pct >= 25) return "Encoding video";
return "Analyzing frames";
}
function useTrackWidth() {
const ref = useRef<HTMLDivElement | null>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) setWidth(entry.contentRect.width);
});
ro.observe(el);
setWidth(el.getBoundingClientRect().width);
return () => ro.disconnect();
}, []);
return { ref, width };
}
const TONES: Record<Tone, string> = {
indigo: "from-indigo-500 to-violet-500",
emerald: "from-emerald-500 to-emerald-400",
sky: "from-sky-500 to-indigo-500",
};
function StatBar({ label, value, tone }: { label: string; value: number; tone: Tone }) {
const reduce = useReducedMotion();
const { ref, width } = useTrackWidth();
const v = Math.max(0, Math.min(100, value));
const text = `${label} · ${Math.round(v)}%`;
return (
<div
ref={ref}
role="progressbar"
aria-label={label}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(v)}
className="relative h-7 w-full overflow-hidden rounded-full bg-slate-200 ring-1 ring-inset ring-slate-300/70 dark:bg-slate-800 dark:ring-slate-700/70"
>
<div className="absolute inset-0 flex items-center justify-center px-3">
<span className="whitespace-nowrap text-[11px] font-medium tracking-tight text-slate-600 tabular-nums dark:text-slate-300">
{text}
</span>
</div>
<motion.div
className={`absolute inset-y-0 left-0 overflow-hidden rounded-full bg-gradient-to-r ${TONES[tone]}`}
initial={false}
animate={{ width: `${v}%` }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 150, damping: 26 }}
>
<div
className="absolute inset-y-0 left-0 flex items-center justify-center px-3"
style={{ width: width || undefined }}
>
<span className="whitespace-nowrap text-[11px] font-semibold tracking-tight text-white tabular-nums">
{text}
</span>
</div>
</motion.div>
</div>
);
}
function IconPlay({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className}>
<path d="M8 5.5v13l11-6.5-11-6.5z" fill="currentColor" />
</svg>
);
}
function IconPause({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className}>
<rect x="7" y="5.5" width="3.4" height="13" rx="1" fill="currentColor" />
<rect x="13.6" y="5.5" width="3.4" height="13" rx="1" fill="currentColor" />
</svg>
);
}
function IconReplay({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M4 12a8 8 0 1 1 2.5 5.8" />
<path d="M4 20v-4h4" />
</svg>
);
}
function IconReset({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M9 4H5v4" />
<path d="M5 8a8 8 0 1 1-1.2 6" />
</svg>
);
}
function IconClapper({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M3 8h18v11a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8z" />
<path d="M3 8l2.4-3.4 3.4.9L6.4 8.9 3 8z" />
<path d="M8.2 5.5l3.4.9-2.4 3.4" />
<path d="M13.4 6.7l3.4.9-2.4 3.4" />
</svg>
);
}
function IconFilm({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<rect x="4" y="4" width="16" height="16" rx="2" />
<path d="M8 4v16M16 4v16M4 9h4M16 9h4M4 15h4M16 15h4" />
</svg>
);
}
function IconDrive({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<rect x="3" y="13" width="18" height="7" rx="2" />
<path d="M6 13l2.2-8h7.6L18 13" />
<circle cx="7.5" cy="16.5" r="0.9" fill="currentColor" stroke="none" />
</svg>
);
}
function IconClock({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7.5V12l3 2" />
</svg>
);
}
export default function ProgLabelInside() {
const reduce = useReducedMotion();
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<Status>("idle");
const [speed, setSpeed] = useState<Speed>(1);
const [announce, setAnnounce] = useState("Ready to export Sunset_Reel_v4.mp4.");
const { ref: trackRef, width: trackW } = useTrackWidth();
const speedRefs = useRef<Array<HTMLButtonElement | null>>([]);
const lastPhase = useRef("");
useEffect(() => {
if (status !== "running") return;
const id = window.setInterval(() => {
setProgress((p) => Math.min(100, p + 0.8 * speed));
}, 110);
return () => window.clearInterval(id);
}, [status, speed]);
useEffect(() => {
if (progress >= 100 && status === "running") setStatus("done");
}, [progress, status]);
useEffect(() => {
if (status === "done") {
setAnnounce("Export complete. Sunset_Reel_v4.mp4 is ready to download.");
return;
}
const ph = phaseName(progress);
if (ph !== lastPhase.current && status === "running") {
lastPhase.current = ph;
setAnnounce(`${ph}, ${Math.round(progress)} percent complete.`);
}
}, [progress, status]);
const clamped = Math.max(0, Math.min(100, progress));
const pct = Math.round(clamped);
const framesDone = Math.round((clamped / 100) * TOTAL_FRAMES);
const mbDone = ((clamped / 100) * TOTAL_MB).toFixed(1);
const etaSec = Math.ceil(((100 - clamped) * 0.7) / speed);
const etaText =
clamped >= 100
? "00:00"
: `${String(Math.floor(etaSec / 60)).padStart(2, "0")}:${String(etaSec % 60).padStart(2, "0")}`;
const gpu =
status === "running" ? Math.round(74 + 20 * Math.abs(Math.sin(clamped / 6))) : status === "done" ? 6 : 0;
const buffer =
status === "running" ? Math.round(46 + 48 * Math.abs(Math.sin(clamped / 4 + 1))) : status === "done" ? 100 : 0;
const barLabel = `${pct}% · ${phaseName(clamped)}`;
const primary =
status === "running"
? ({ text: "Pause", kind: "pause" } as const)
: status === "done" || clamped >= 100
? ({ text: "Replay", kind: "replay" } as const)
: clamped > 0
? ({ text: "Resume", kind: "play" } as const)
: ({ text: "Start export", kind: "play" } as const);
function handlePrimary() {
if (status === "running") {
setStatus("paused");
return;
}
if (status === "done" || clamped >= 100) {
lastPhase.current = "";
setProgress(0);
setStatus("running");
return;
}
setStatus("running");
}
function handleReset() {
lastPhase.current = "";
setProgress(0);
setStatus("idle");
setAnnounce("Export reset. Ready to start again.");
}
function handleScrub(e: ChangeEvent<HTMLInputElement>) {
const v = Number(e.target.value);
setProgress(v);
setStatus(v >= 100 ? "done" : "paused");
}
function handleSpeedKey(e: KeyboardEvent<HTMLButtonElement>, idx: number) {
let next = idx;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (idx + 1) % SPEEDS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (idx - 1 + SPEEDS.length) % SPEEDS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = SPEEDS.length - 1;
else return;
e.preventDefault();
setSpeed(SPEEDS[next]);
speedRefs.current[next]?.focus();
}
const statusMeta =
status === "running"
? { label: "Rendering", cls: "bg-indigo-100 text-indigo-700 ring-indigo-500/20 dark:bg-indigo-500/15 dark:text-indigo-300" }
: status === "paused"
? { label: "Paused", cls: "bg-amber-100 text-amber-700 ring-amber-500/20 dark:bg-amber-500/15 dark:text-amber-300" }
: status === "done"
? { label: "Complete", cls: "bg-emerald-100 text-emerald-700 ring-emerald-500/20 dark:bg-emerald-500/15 dark:text-emerald-300" }
: { label: "Ready", cls: "bg-slate-100 text-slate-600 ring-slate-500/20 dark:bg-slate-800 dark:text-slate-300" };
const stats = [
{ icon: <IconFilm className="h-4 w-4" />, k: "Frames", v: `${framesDone.toLocaleString()} / ${TOTAL_FRAMES.toLocaleString()}` },
{ icon: <IconDrive className="h-4 w-4" />, k: "Output", v: `${mbDone} MB` },
{ icon: <IconClock className="h-4 w-4" />, k: "Time left", v: etaText },
];
return (
<section
aria-labelledby="pli-title"
className="relative w-full bg-gradient-to-b from-slate-50 to-white px-4 py-16 text-slate-900 sm:py-24 dark:from-slate-950 dark:to-slate-900 dark:text-slate-100"
>
<style>{`
@keyframes pli-sweep {
0% { transform: translateX(-110%); }
100% { transform: translateX(240%); }
}
@keyframes pli-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
.pli-sweep, .pli-blink { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-900/5 sm:p-8 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30">
{/* Header */}
<div className="flex items-start gap-4">
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/30">
<IconClapper className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Render Queue
</p>
<h2 id="pli-title" className="mt-0.5 truncate text-lg font-bold tracking-tight sm:text-xl">
Sunset_Reel_v4.mp4
</h2>
<p className="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
3840 × 2160 · H.264 · 24 fps
</p>
</div>
<span
className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${statusMeta.cls}`}
>
<span
className={`h-1.5 w-1.5 rounded-full bg-current ${status === "running" && !reduce ? "pli-blink" : ""}`}
style={status === "running" && !reduce ? { animation: "pli-blink 1.1s ease-in-out infinite" } : undefined}
/>
{statusMeta.label}
</span>
</div>
{/* Main progress bar with inner label */}
<div className="mt-7">
<div
ref={trackRef}
role="progressbar"
aria-label="Video export progress"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={pct}
aria-valuetext={`${pct} percent — ${phaseName(clamped)}`}
className="relative h-11 w-full overflow-hidden rounded-full bg-slate-200 ring-1 ring-inset ring-slate-300/70 dark:bg-slate-800 dark:ring-slate-700/60"
>
<div className="absolute inset-0 flex items-center justify-center px-4">
<span className="whitespace-nowrap text-sm font-semibold tracking-tight text-slate-600 tabular-nums dark:text-slate-300">
{barLabel}
</span>
</div>
<motion.div
className="absolute inset-y-0 left-0 overflow-hidden rounded-full bg-gradient-to-r from-indigo-600 via-indigo-500 to-violet-500"
initial={false}
animate={{ width: `${clamped}%` }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 140, damping: 24 }}
>
<div
className="absolute inset-y-0 left-0 flex items-center justify-center px-4"
style={{ width: trackW || undefined }}
>
<span className="whitespace-nowrap text-sm font-semibold tracking-tight text-white tabular-nums">
{barLabel}
</span>
</div>
{status === "running" && !reduce ? (
<span
className="pli-sweep pointer-events-none absolute inset-y-0 left-0 w-24 bg-gradient-to-r from-transparent via-white/35 to-transparent"
style={{ animation: "pli-sweep 1.6s linear infinite" }}
/>
) : null}
</motion.div>
</div>
{/* Stats */}
<dl className="mt-4 grid grid-cols-3 gap-3">
{stats.map((s) => (
<div
key={s.k}
className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-3 dark:border-slate-800 dark:bg-slate-800/40"
>
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
<span className="text-indigo-500 dark:text-indigo-400">{s.icon}</span>
{s.k}
</dt>
<dd className="mt-1 text-sm font-semibold tabular-nums text-slate-800 dark:text-slate-100">
{s.v}
</dd>
</div>
))}
</dl>
{/* Secondary meters, also labelled inside */}
<div className="mt-4 space-y-2.5">
<StatBar label="GPU utilisation" value={gpu} tone="indigo" />
<StatBar label="Encoder buffer" value={buffer} tone="emerald" />
</div>
</div>
{/* Scrub control */}
<div className="mt-7">
<label
htmlFor="pli-scrub"
className="mb-2 block text-xs font-medium text-slate-500 dark:text-slate-400"
>
Scrub progress
</label>
<input
id="pli-scrub"
type="range"
min={0}
max={100}
step={0.5}
value={clamped}
onChange={handleScrub}
aria-label="Scrub export progress"
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-600 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"
/>
</div>
{/* Controls */}
<div className="mt-6 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={handlePrimary}
className="inline-flex items-center gap-2 rounded-full bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 outline-none transition-colors hover:bg-indigo-500 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"
>
{primary.kind === "pause" ? (
<IconPause className="h-4 w-4" />
) : primary.kind === "replay" ? (
<IconReplay className="h-4 w-4" />
) : (
<IconPlay className="h-4 w-4" />
)}
{primary.text}
</button>
<button
type="button"
onClick={handleReset}
disabled={clamped === 0 && status === "idle"}
className="inline-flex items-center gap-2 rounded-full border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 outline-none transition-colors hover:bg-slate-50 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-45 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
>
<IconReset className="h-4 w-4" />
Reset
</button>
<div
role="radiogroup"
aria-label="Render speed"
className="ml-auto inline-flex items-center rounded-full border border-slate-300 bg-slate-100 p-1 dark:border-slate-700 dark:bg-slate-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)}
onKeyDown={(e) => handleSpeedKey(e, i)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-800 ${
active
? "bg-white text-indigo-700 shadow-sm dark:bg-slate-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>
<p aria-live="polite" className="sr-only">
{announce}
</p>
</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

Indeterminate Progress
Originalindeterminate progress bar

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.

