Empty Success
Original · freesuccess / done empty state
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/empty-success.json"use client";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type StepStatus = "passed" | "skipped";
type Step = {
id: string;
label: string;
detail: string;
duration: string;
status: StepStatus;
};
const RUN_ID = "rel_8f42c1";
const STEPS: Step[] = [
{
id: "st1",
label: "Bundle and type-check",
detail: "1,204 modules, no new type errors",
duration: "42s",
status: "passed",
},
{
id: "st2",
label: "Unit and integration suites",
detail: "318 tests, 0 failures, 2 quarantined",
duration: "1m 09s",
status: "passed",
},
{
id: "st3",
label: "Visual diff against baseline",
detail: "94 snapshots, 3 intentional changes approved by Priya",
duration: "27s",
status: "passed",
},
{
id: "st4",
label: "Canary to 5% of traffic",
detail: "Error rate held at 0.02%, p95 latency 214ms",
duration: "4m 00s",
status: "passed",
},
{
id: "st5",
label: "Full rollout to 12 regions",
detail: "Frankfurt and São Paulo drained last, both healthy",
duration: "1m 51s",
status: "passed",
},
{
id: "st6",
label: "Database migration",
detail: "No pending migrations in this release",
duration: "0s",
status: "skipped",
},
];
const STATS: { id: string; value: string; label: string }[] = [
{ id: "k1", value: "5 / 5", label: "Checks passed" },
{ id: "k2", value: "8m 09s", label: "Pipeline time" },
{ id: "k3", value: "0", label: "Rollbacks today" },
];
const ROLLBACK_WINDOW_SECONDS = 30;
export default function EmptySuccess() {
const reduceMotion = useReducedMotion();
const panelId = useId();
const detailsId = `${panelId}-details`;
const statusId = `${panelId}-status`;
const [rolledBack, setRolledBack] = useState(false);
const [detailsOpen, setDetailsOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(ROLLBACK_WINDOW_SECONDS);
const [announcement, setAnnouncement] = useState("");
const copyTimer = useRef<number | null>(null);
useEffect(() => {
if (rolledBack || secondsLeft <= 0) return;
const tick = window.setInterval(() => {
setSecondsLeft((current) => (current <= 1 ? 0 : current - 1));
}, 1000);
return () => window.clearInterval(tick);
}, [rolledBack, secondsLeft]);
useEffect(() => {
return () => {
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
};
}, []);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(RUN_ID);
setCopied(true);
setAnnouncement(`Release ID ${RUN_ID} copied to clipboard.`);
} catch {
setCopied(false);
setAnnouncement("Copy failed. Select the release ID manually.");
}
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 2000);
}, []);
const handleRollback = useCallback(() => {
setRolledBack(true);
setDetailsOpen(false);
setAnnouncement("Rolled back to version 4.7.2. Traffic is serving the previous build.");
}, []);
const handleRedeploy = useCallback(() => {
setRolledBack(false);
setSecondsLeft(ROLLBACK_WINDOW_SECONDS);
setAnnouncement(`Release ${RUN_ID} is live again across 12 regions.`);
}, []);
const windowOpen = !rolledBack && secondsLeft > 0;
const progress = Math.max(0, Math.min(1, secondsLeft / ROLLBACK_WINDOW_SECONDS));
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-6 py-24 text-slate-900 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes esucc-ring {
0% { transform: scale(0.82); opacity: 0.55; }
70% { transform: scale(1.35); opacity: 0; }
100% { transform: scale(1.35); opacity: 0; }
}
@keyframes esucc-draw {
from { stroke-dashoffset: 34; }
to { stroke-dashoffset: 0; }
}
@keyframes esucc-drift {
0%, 100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(0, -8px, 0); }
}
.esucc-ring {
animation: esucc-ring 2.6s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
.esucc-check {
stroke-dasharray: 34;
stroke-dashoffset: 34;
animation: esucc-draw 700ms cubic-bezier(0.65, 0, 0.35, 1) 260ms forwards;
}
.esucc-drift {
animation: esucc-drift 6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.esucc-ring, .esucc-drift { animation: none; }
.esucc-check { animation: none; stroke-dashoffset: 0; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 mx-auto h-72 max-w-3xl bg-[radial-gradient(60%_100%_at_50%_0%,rgba(16,185,129,0.18),transparent_70%)] dark:bg-[radial-gradient(60%_100%_at_50%_0%,rgba(16,185,129,0.16),transparent_70%)]"
/>
<div className="relative mx-auto w-full max-w-3xl">
<motion.div
initial={reduceMotion ? false : { opacity: 0, y: 18 }}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="rounded-3xl border border-slate-200 bg-white p-8 shadow-[0_1px_2px_rgba(15,23,42,0.04),0_24px_60px_-30px_rgba(15,23,42,0.35)] sm:p-12 dark:border-slate-800 dark:bg-slate-900 dark:shadow-[0_24px_60px_-30px_rgba(0,0,0,0.9)]"
>
<div className="flex flex-col items-center text-center">
<div className="esucc-drift relative flex h-20 w-20 items-center justify-center">
<span
aria-hidden="true"
className={`absolute inset-0 rounded-full ${
rolledBack ? "bg-amber-400/30" : "bg-emerald-400/30"
} ${rolledBack ? "" : "esucc-ring"}`}
/>
<span
aria-hidden="true"
className={`absolute inset-0 rounded-full ring-1 ${
rolledBack
? "bg-amber-50 ring-amber-200 dark:bg-amber-500/10 dark:ring-amber-500/30"
: "bg-emerald-50 ring-emerald-200 dark:bg-emerald-500/10 dark:ring-emerald-500/30"
}`}
/>
{rolledBack ? (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="relative h-9 w-9 text-amber-600 dark:text-amber-400"
>
<path
d="M4 8h8.5a5.5 5.5 0 1 1 0 11H8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 4.5 4 8l3.5 3.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="relative h-9 w-9 text-emerald-600 dark:text-emerald-400"
>
<path
className="esucc-check"
d="M5 12.5 10 17.5 19 7"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span
className={`mt-6 inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-medium tracking-wide uppercase ${
rolledBack
? "bg-amber-50 text-amber-700 ring-1 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-500/25"
: "bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/25"
}`}
>
<span
aria-hidden="true"
className={`h-1.5 w-1.5 rounded-full ${rolledBack ? "bg-amber-500" : "bg-emerald-500"}`}
/>
{rolledBack ? "Serving v4.7.2" : "Queue empty"}
</span>
<h2 className="mt-4 text-2xl font-semibold tracking-tight text-balance sm:text-3xl">
{rolledBack ? "Rolled back to v4.7.2" : "Everything shipped. Nothing left to do."}
</h2>
<p className="mt-3 max-w-xl text-sm leading-relaxed text-pretty text-slate-600 sm:text-base dark:text-slate-400">
{rolledBack
? "All 12 regions are back on the previous build. The pipeline kept the artifacts, so you can put v4.8.0 back out whenever you're ready."
: "Release v4.8.0 finished at 3:14 PM and is serving all 12 regions. Every check passed on the first attempt, so there's nothing in this queue that needs you."}
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2 text-xs">
<span className="text-slate-500 dark:text-slate-500">Release ID</span>
<button
type="button"
onClick={handleCopy}
className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-2.5 py-1.5 font-mono text-xs text-slate-700 transition-colors hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
{RUN_ID}
{copied ? (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400">
<path d="M5 12.5 10 17.5 19 7" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5">
<rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.8" />
<path d="M15 5H6a2 2 0 0 0-2 2v9" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
)}
<span className="sr-only">{copied ? "Release ID copied" : "Copy release ID"}</span>
</button>
</div>
</div>
<dl className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-3">
{STATS.map((stat) => (
<div
key={stat.id}
className="rounded-2xl border border-slate-200 bg-slate-50/70 px-4 py-4 text-center dark:border-slate-800 dark:bg-slate-800/40"
>
<dt className="text-xs font-medium tracking-wide text-slate-500 uppercase dark:text-slate-500">
{stat.label}
</dt>
<dd className="mt-1.5 font-mono text-lg font-semibold text-slate-900 tabular-nums dark:text-slate-100">
{stat.value}
</dd>
</div>
))}
</dl>
{windowOpen ? (
<div className="mt-6 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50/70 dark:border-amber-500/25 dark:bg-amber-500/[0.07]">
<div className="flex flex-col gap-3 px-4 py-3.5 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-amber-900 dark:text-amber-200">
Rollback window closes in{" "}
<span className="font-mono font-semibold tabular-nums">{secondsLeft}s</span>. After
that, v4.7.2 gets garbage collected.
</p>
<button
type="button"
onClick={handleRollback}
className="inline-flex shrink-0 items-center justify-center gap-1.5 rounded-lg border border-amber-300 bg-white px-3 py-1.5 text-sm font-medium text-amber-800 transition-colors hover:bg-amber-100 focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-amber-50 focus-visible:outline-none dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200 dark:hover:bg-amber-500/20 dark:focus-visible:ring-offset-slate-900"
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-4 w-4">
<path d="M4 8h8.5a5.5 5.5 0 1 1 0 11H8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.5 4.5 4 8l3.5 3.5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Roll back
</button>
</div>
<div
role="progressbar"
aria-label="Time remaining in rollback window"
aria-valuemin={0}
aria-valuemax={ROLLBACK_WINDOW_SECONDS}
aria-valuenow={secondsLeft}
aria-valuetext={`${secondsLeft} seconds remaining`}
className="h-1 w-full bg-amber-200/60 dark:bg-amber-500/15"
>
<div
className="h-full origin-left bg-amber-500 transition-[width] duration-1000 ease-linear dark:bg-amber-400"
style={{ width: `${progress * 100}%` }}
/>
</div>
</div>
) : null}
{!rolledBack ? (
<div className="mt-6 rounded-2xl border border-slate-200 dark:border-slate-800">
<h3>
<button
type="button"
aria-expanded={detailsOpen}
aria-controls={detailsId}
onClick={() => setDetailsOpen((open) => !open)}
className="flex w-full items-center justify-between gap-3 rounded-2xl px-4 py-3.5 text-left text-sm font-medium text-slate-800 transition-colors hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-inset focus-visible:outline-none dark:text-slate-200 dark:hover:bg-slate-800/40"
>
<span>What ran, and how long it took</span>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className={`h-4 w-4 shrink-0 text-slate-400 transition-transform duration-200 ${
detailsOpen ? "rotate-180" : "rotate-0"
}`}
>
<path d="m6 9 6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</h3>
<div id={detailsId} hidden={!detailsOpen} className="border-t border-slate-200 px-4 py-2 dark:border-slate-800">
<ul className="divide-y divide-slate-100 dark:divide-slate-800/70">
{STEPS.map((step) => (
<li key={step.id} className="flex items-start gap-3 py-3">
{step.status === "passed" ? (
<span
aria-hidden="true"
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400"
>
<svg viewBox="0 0 24 24" fill="none" className="h-3 w-3">
<path d="M5 12.5 10 17.5 19 7" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
) : (
<span
aria-hidden="true"
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-500"
>
<svg viewBox="0 0 24 24" fill="none" className="h-3 w-3">
<path d="M6 12h12" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
</span>
)}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">
{step.label}
<span className="sr-only">{step.status === "passed" ? " — passed" : " — skipped"}</span>
</p>
<p className="mt-0.5 text-xs leading-relaxed text-slate-500 dark:text-slate-500">
{step.detail}
</p>
</div>
<span className="shrink-0 font-mono text-xs text-slate-400 tabular-nums dark:text-slate-500">
{step.duration}
</span>
</li>
))}
</ul>
</div>
</div>
) : null}
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
{rolledBack ? (
<button
type="button"
onClick={handleRedeploy}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white dark:focus-visible:ring-slate-100 dark:focus-visible:ring-offset-slate-900"
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-4 w-4">
<path d="M20 12a8 8 0 1 1-2.34-5.66" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M20 4v4h-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Re-deploy v4.8.0
</button>
) : (
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white dark:focus-visible:ring-slate-100 dark:focus-visible:ring-offset-slate-900"
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-4 w-4">
<path d="M6 4h9l5 5v11a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round" />
<path d="M14 4v6h6M9 14h6M9 17.5h4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Read the v4.8.0 notes
</button>
)}
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white px-5 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:border-slate-700 dark:bg-transparent dark:text-slate-200 dark:hover:bg-slate-800/60 dark:focus-visible:ring-offset-slate-900"
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" className="h-4 w-4">
<rect x="3.5" y="5" width="17" height="15.5" rx="2" stroke="currentColor" strokeWidth="1.7" />
<path d="M3.5 9.5h17M8 3.5V6M16 3.5V6M12 13v3.5h3" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Schedule the next window
</button>
</div>
<p className="mt-6 text-center text-xs text-slate-500 dark:text-slate-500">
Deploys are frozen Fridays after 2:00 PM. The next open window is Monday at 9:00 AM.
</p>
<p id={statusId} aria-live="polite" className="sr-only">
{announcement}
</p>
</motion.div>
</div>
</section>
);
}Dependencies
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 →
Empty No Data
Originalempty data state with illustration

Empty No Results
Originalno results with clear filters

Empty Cart
Originalempty shopping cart state

Empty Inbox
Originalempty inbox / all caught up

Empty First Run
Originalfirst-run get started empty state

Empty Notifications
Originalno notifications state

Empty Error
Originalsomething went wrong empty state

Empty No Connection
Originalno connection empty state

Empty Folder
Originalempty folder with upload CTA

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

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

