Empty First Run
Original · freefirst-run get started 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-first-run.json"use client";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type StepId = "connect" | "invite" | "threshold" | "cli";
type Step = {
id: StepId;
title: string;
summary: string;
detail: string;
minutes: number;
action: string;
hint: string;
};
const CLI_COMMAND = "npm i -g @meridian/cli && meridian login";
const STEPS: Step[] = [
{
id: "connect",
title: "Connect your first service",
summary: "Point Meridian at a health endpoint and we start polling it.",
detail:
"Paste any HTTPS URL that returns a 2xx when your service is healthy. Meridian polls it every 30 seconds from four regions and records latency, status code, and TLS expiry. Nothing else needs to change in your app.",
minutes: 2,
action: "Add a service",
hint: "Most teams start with their production API root.",
},
{
id: "invite",
title: "Invite your on-call rotation",
summary: "Alerts are useless if they reach an empty room.",
detail:
"Add teammates by email and assign a rotation order. The first responder gets paged, and if there is no acknowledgement within five minutes, the page escalates to the next person on the list.",
minutes: 1,
action: "Invite teammates",
hint: "Two people is the minimum we recommend for escalation.",
},
{
id: "threshold",
title: "Set an alert threshold",
summary: "Decide what counts as broken before it breaks.",
detail:
"Choose the p95 latency and error rate that should trigger a page. Meridian defaults to 800 ms p95 over three consecutive checks, which is quiet enough to sleep through and loud enough to matter.",
minutes: 3,
action: "Configure alerts",
hint: "You can tune this per service later without losing history.",
},
{
id: "cli",
title: "Install the CLI",
summary: "Ship deploy markers so incidents line up with releases.",
detail:
"The CLI writes a deploy marker onto your latency charts, so a spike at 14:02 is immediately traceable to the release at 14:01. Add the command to your deploy pipeline once and forget it.",
minutes: 2,
action: "View install docs",
hint: "Works with GitHub Actions, GitLab CI, and plain shell scripts.",
},
];
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M5 12.5l4.5 4.5L19 7" />
</svg>
);
}
function ChevronIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M6 9l6 6 6-6" />
</svg>
);
}
function ArrowIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M5 12h13" />
<path d="M12 5l7 7-7 7" />
</svg>
);
}
export default function EmptyFirstRun() {
const reduce = useReducedMotion();
const headingId = useId();
const listId = useId();
const statusId = useId();
const [done, setDone] = useState<StepId[]>([]);
const [open, setOpen] = useState<StepId | null>("connect");
const [copied, setCopied] = useState(false);
const headerRefs = useRef<Array<HTMLButtonElement | null>>([]);
const copyTimer = useRef<number | null>(null);
useEffect(() => {
return () => {
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
};
}, []);
const isDone = useCallback((id: StepId) => done.includes(id), [done]);
const toggleDone = useCallback((id: StepId) => {
setDone((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
);
}, []);
const completed = done.length;
const total = STEPS.length;
const allDone = completed === total;
const percent = Math.round((completed / total) * 100);
const nextStep = useMemo<Step | null>(
() => STEPS.find((s) => !done.includes(s.id)) ?? null,
[done],
);
const minutesLeft = useMemo(
() =>
STEPS.filter((s) => !done.includes(s.id)).reduce(
(sum, s) => sum + s.minutes,
0,
),
[done],
);
const copyCommand = useCallback(async () => {
try {
await navigator.clipboard.writeText(CLI_COMMAND);
setCopied(true);
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
}, []);
function onHeaderKeyDown(
e: ReactKeyboardEvent<HTMLButtonElement>,
i: number,
) {
let next = -1;
if (e.key === "ArrowDown") next = (i + 1) % STEPS.length;
else if (e.key === "ArrowUp") next = (i - 1 + STEPS.length) % STEPS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = STEPS.length - 1;
else return;
e.preventDefault();
headerRefs.current[next]?.focus();
}
const RADIUS = 34;
const CIRC = 2 * Math.PI * RADIUS;
const offset = CIRC * (1 - completed / total);
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100"
>
<style>{`
@keyframes efr-drift {
0%, 100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(0, -10px, 0); }
}
@keyframes efr-pulse-ring {
0% { transform: scale(0.9); opacity: 0.65; }
70% { transform: scale(1.5); opacity: 0; }
100% { transform: scale(1.5); opacity: 0; }
}
@keyframes efr-sweep {
0% { transform: translateX(-110%); }
100% { transform: translateX(320%); }
}
@keyframes efr-pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.12); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes efr-spark {
0% { transform: translateY(0) scale(0.6); opacity: 0; }
25% { opacity: 1; }
100% { transform: translateY(-46px) scale(1); opacity: 0; }
}
.efr-drift { animation: efr-drift 7s ease-in-out infinite; }
.efr-pulse-ring { animation: efr-pulse-ring 2.4s ease-out infinite; }
.efr-sweep { animation: efr-sweep 2.8s ease-in-out infinite; }
.efr-pop { animation: efr-pop 420ms cubic-bezier(0.22, 1, 0.36, 1) both; }
.efr-spark { animation: efr-spark 1.6s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.efr-drift,
.efr-pulse-ring,
.efr-sweep,
.efr-pop,
.efr-spark {
animation: none !important;
}
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-500/10 blur-3xl dark:bg-indigo-500/15"
/>
<div className="relative mx-auto max-w-6xl">
{/* Header */}
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-xl">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
<span className="relative flex h-2 w-2">
<span
aria-hidden="true"
className="efr-pulse-ring absolute inline-flex h-full w-full rounded-full bg-emerald-500"
/>
<span
aria-hidden="true"
className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"
/>
</span>
Workspace created · nothing monitored yet
</span>
<h2
id={headingId}
className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white"
>
Welcome to Meridian, Priya.
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Your workspace is empty on purpose — we don't guess at
your infrastructure. Four short steps and you'll have live
latency charts, a working escalation path, and deploy markers on
every release.
</p>
</div>
{/* Progress ring */}
<div className="flex shrink-0 items-center gap-4">
<div className="relative h-24 w-24">
<svg viewBox="0 0 80 80" className="h-24 w-24 -rotate-90">
<circle
cx="40"
cy="40"
r={RADIUS}
fill="none"
strokeWidth="7"
className="stroke-slate-200 dark:stroke-slate-800"
/>
<motion.circle
cx="40"
cy="40"
r={RADIUS}
fill="none"
strokeWidth="7"
strokeLinecap="round"
strokeDasharray={CIRC}
className={
allDone
? "stroke-emerald-500"
: "stroke-indigo-500 dark:stroke-indigo-400"
}
initial={false}
animate={{ strokeDashoffset: offset }}
transition={
reduce
? { duration: 0 }
: { duration: 0.55, ease: [0.22, 1, 0.36, 1] }
}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
{percent}%
</span>
<span className="text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
setup
</span>
</div>
</div>
<div className="text-sm">
<p className="font-medium text-slate-900 dark:text-white">
{completed} of {total} done
</p>
<p className="mt-0.5 text-slate-500 dark:text-slate-400">
{allDone
? "You're fully set up."
: `About ${minutesLeft} min left`}
</p>
</div>
</div>
</div>
<p id={statusId} role="status" aria-live="polite" className="sr-only">
{allDone
? "Setup complete. All four steps are done."
: `${completed} of ${total} setup steps complete. Next up: ${
nextStep ? nextStep.title : ""
}.`}
</p>
<div className="mt-12 grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)] lg:gap-12">
{/* Checklist */}
<div>
<div className="flex items-baseline justify-between gap-4">
<h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Get started
</h3>
{completed > 0 ? (
<button
type="button"
onClick={() => setDone([])}
className="rounded text-xs font-medium text-slate-500 underline-offset-4 transition hover:text-indigo-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-400 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
>
Reset checklist
</button>
) : null}
</div>
<ul id={listId} className="mt-4 space-y-3">
{STEPS.map((step, i) => {
const stepDone = isDone(step.id);
const expanded = open === step.id;
const isNext = !stepDone && nextStep?.id === step.id;
return (
<li
key={step.id}
className={[
"rounded-2xl border bg-white shadow-sm transition-colors dark:bg-slate-900",
stepDone
? "border-emerald-200 dark:border-emerald-500/30"
: isNext
? "border-indigo-300 dark:border-indigo-500/50"
: "border-slate-200 dark:border-slate-800",
].join(" ")}
>
<div className="flex items-start gap-3 p-4">
<button
type="button"
role="checkbox"
aria-checked={stepDone}
aria-label={`Mark "${step.title}" as ${
stepDone ? "not done" : "done"
}`}
onClick={() => toggleDone(step.id)}
className={[
"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 transition 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",
stepDone
? "border-emerald-500 bg-emerald-500 text-white"
: "border-slate-300 bg-white text-transparent hover:border-indigo-500 dark:border-slate-600 dark:bg-slate-900",
].join(" ")}
>
{stepDone ? (
<CheckIcon className="efr-pop h-3.5 w-3.5" />
) : (
<span className="text-xs font-semibold text-slate-400 dark:text-slate-500">
{i + 1}
</span>
)}
</button>
<button
type="button"
ref={(el) => {
headerRefs.current[i] = el;
}}
aria-expanded={expanded}
aria-controls={`${listId}-panel-${step.id}`}
id={`${listId}-header-${step.id}`}
onClick={() => setOpen(expanded ? null : step.id)}
onKeyDown={(e) => onHeaderKeyDown(e, i)}
className="flex min-w-0 flex-1 items-start gap-3 rounded-lg text-left 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"
>
<span className="min-w-0 flex-1">
<span className="flex flex-wrap items-center gap-2">
<span
className={[
"text-[15px] font-semibold",
stepDone
? "text-slate-400 line-through decoration-slate-300 dark:text-slate-500 dark:decoration-slate-600"
: "text-slate-900 dark:text-white",
].join(" ")}
>
{step.title}
</span>
{isNext ? (
<span className="rounded-md bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
Next up
</span>
) : null}
<span className="text-xs text-slate-400 dark:text-slate-500">
{step.minutes} min
</span>
</span>
<span className="mt-1 block text-sm text-slate-600 dark:text-slate-400">
{step.summary}
</span>
</span>
<ChevronIcon
className={[
"mt-1 h-4 w-4 shrink-0 text-slate-400 transition-transform duration-200 dark:text-slate-500",
expanded ? "rotate-180" : "",
].join(" ")}
/>
</button>
</div>
<AnimatePresence initial={false}>
{expanded ? (
<motion.div
key="panel"
id={`${listId}-panel-${step.id}`}
role="region"
aria-labelledby={`${listId}-header-${step.id}`}
initial={reduce ? false : { height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={
reduce
? { opacity: 0 }
: { height: 0, opacity: 0 }
}
transition={
reduce
? { duration: 0 }
: { duration: 0.26, ease: [0.22, 1, 0.36, 1] }
}
className="overflow-hidden"
>
<div className="border-t border-slate-100 px-4 pb-4 pl-[3.25rem] pt-4 dark:border-slate-800">
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{step.detail}
</p>
{step.id === "cli" ? (
<div className="mt-3 flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950">
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap px-2 py-1 font-mono text-xs text-slate-700 dark:text-slate-300">
{CLI_COMMAND}
</code>
<button
type="button"
onClick={copyCommand}
className="shrink-0 rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-700 transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
>
{copied ? "Copied" : "Copy"}
</button>
</div>
) : null}
<div className="mt-4 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => toggleDone(step.id)}
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition 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 active:scale-[0.98] dark:focus-visible:ring-offset-slate-900"
>
{stepDone ? "Marked done" : step.action}
<ArrowIcon className="h-4 w-4" />
</button>
<span className="text-xs text-slate-500 dark:text-slate-400">
{step.hint}
</span>
</div>
</div>
</motion.div>
) : null}
</AnimatePresence>
</li>
);
})}
</ul>
<div className="mt-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-slate-500 dark:text-slate-400">
<span>
Use{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
↑
</kbd>{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
↓
</kbd>{" "}
to move between steps.
</span>
<a
href="#meridian-docs"
className="rounded font-medium text-slate-600 underline decoration-dotted underline-offset-4 transition hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-300 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
>
Skip setup and explore an example workspace
</a>
</div>
</div>
{/* Live preview canvas */}
<div className="lg:sticky lg:top-8 lg:self-start">
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Your dashboard
</p>
<span
className={[
"rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition-colors",
allDone
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
: "bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400",
].join(" ")}
>
{allDone ? "Live" : "Preview"}
</span>
</div>
<div className="mt-4 space-y-3">
<PreviewRow active={isDone("connect")} reduce={reduce}>
<div className="flex items-center gap-3">
<span className="h-2 w-2 shrink-0 rounded-full bg-emerald-500" />
<span className="min-w-0 flex-1 truncate text-sm font-medium text-slate-900 dark:text-white">
api.meridian.dev/health
</span>
<span className="shrink-0 text-xs tabular-nums text-slate-500 dark:text-slate-400">
142 ms
</span>
</div>
</PreviewRow>
<PreviewRow active={isDone("invite")} reduce={reduce}>
<div className="flex items-center gap-3">
<span className="flex -space-x-1.5">
{["PR", "DK", "AM"].map((initials) => (
<span
key={initials}
className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-indigo-100 text-[9px] font-semibold text-indigo-700 dark:border-slate-900 dark:bg-indigo-500/20 dark:text-indigo-300"
>
{initials}
</span>
))}
</span>
<span className="text-sm text-slate-600 dark:text-slate-400">
On-call rotation · 3 responders
</span>
</div>
</PreviewRow>
<PreviewRow active={isDone("threshold")} reduce={reduce}>
<div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Alert at p95
</span>
<span className="text-sm font-semibold tabular-nums text-slate-900 dark:text-white">
800 ms
</span>
</div>
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<div className="h-full w-[38%] rounded-full bg-gradient-to-r from-emerald-500 to-amber-400" />
</div>
</div>
</PreviewRow>
<PreviewRow active={isDone("cli")} reduce={reduce}>
<div className="flex items-center gap-3">
<span className="rounded bg-violet-100 px-1.5 py-0.5 font-mono text-[10px] font-semibold text-violet-700 dark:bg-violet-500/15 dark:text-violet-300">
v2.14.0
</span>
<span className="text-sm text-slate-600 dark:text-slate-400">
Deploy marker · 2 minutes ago
</span>
</div>
</PreviewRow>
</div>
{/* Empty-canvas placeholder */}
{completed === 0 ? (
<div className="relative mt-4 overflow-hidden rounded-xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-8 text-center dark:border-slate-700 dark:bg-slate-950/40">
<span
aria-hidden="true"
className="efr-sweep pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-transparent via-white/70 to-transparent dark:via-white/5"
/>
<svg
viewBox="0 0 96 60"
role="img"
aria-label="Empty dashboard placeholder"
className="efr-drift mx-auto h-14 w-auto"
>
<g
className="text-slate-300 dark:text-slate-700"
stroke="currentColor"
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
>
<rect
x="4"
y="6"
width="88"
height="48"
rx="8"
strokeDasharray="6 8"
/>
</g>
<polyline
points="16,42 32,34 46,38 60,22 76,28"
fill="none"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-indigo-400/60 dark:stroke-indigo-400/40"
/>
</svg>
<p className="relative mt-3 text-xs text-slate-500 dark:text-slate-400">
No data yet. Complete a step and this panel fills in.
</p>
</div>
) : null}
{/* Completion celebration */}
<AnimatePresence initial={false}>
{allDone ? (
<motion.div
key="celebrate"
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.32, ease: [0.22, 1, 0.36, 1] }
}
className="relative mt-5 overflow-hidden rounded-xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/30 dark:bg-emerald-500/10"
>
<span aria-hidden="true" className="pointer-events-none absolute inset-0">
{[12, 30, 50, 68, 86].map((left, i) => (
<span
key={left}
className="efr-spark absolute bottom-2 h-1.5 w-1.5 rounded-full bg-emerald-400"
style={{
left: `${left}%`,
animationDelay: `${i * 0.22}s`,
}}
/>
))}
</span>
<p className="relative text-sm font-semibold text-emerald-900 dark:text-emerald-200">
Monitoring is live.
</p>
<p className="relative mt-1 text-sm text-emerald-800/90 dark:text-emerald-200/80">
Meridian is polling from four regions and your rotation is
armed. First report lands in your inbox on Monday.
</p>
<a
href="#meridian-dashboard"
className="relative mt-3 inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-3.5 py-2 text-xs font-semibold text-white transition hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:focus-visible:ring-offset-slate-900"
>
Open the dashboard
<ArrowIcon className="h-3.5 w-3.5" />
</a>
</motion.div>
) : null}
</AnimatePresence>
</div>
<p className="mt-4 px-1 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
Everything here is reversible. Delete a service and its history
goes with it — no support ticket, no retention trap.
</p>
</div>
</div>
</div>
</section>
);
}
function PreviewRow({
active,
reduce,
children,
}: {
active: boolean;
reduce: boolean | null;
children: ReactNode;
}) {
return (
<motion.div
initial={false}
animate={{ opacity: active ? 1 : 0.35 }}
transition={reduce ? { duration: 0 } : { duration: 0.3 }}
className={[
"rounded-xl border p-3 transition-colors",
active
? "border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800/50"
: "border-dashed border-slate-200 bg-slate-50 grayscale dark:border-slate-800 dark:bg-slate-950/40",
].join(" ")}
>
{children}
</motion.div>
);
}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 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

Empty Success
Originalsuccess / done empty state

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.

