A dashboard comes apart into its shell, sidebar, live cards, command menu and a teammate's cursor, walks through each one, then lands back as the screenshot it started as. Hover a layer once it's open, or click a label to jump to it.
@keyframeui
registry/demos/exploded-ui-demo.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { ExplodedUI } from "@/components/ui/exploded-ui";
// Everything inside the screenshot is sized off the plane's width (Tailwind's
// spacing, text and radius scales are set in container units), so it scales
// like an image while staying real, theme-aware HTML.
const screen =
"[--spacing:0.4cqw] [--text-xs:1.2cqw] [--text-sm:1.4cqw] [--text-base:1.6cqw] [--text-lg:1.9cqw] [--text-2xl:2.6cqw] [--radius-md:0.6cqw] [--radius-lg:0.8cqw] [--radius-xl:1.2cqw]";
// For a full-page section, drop the scroll box, `containerRef` and
// `stageClassName`: the stage fills the window and sticks while the page scrolls.
export function ExplodedUIDemo() {
const scrollRef = React.useRef<HTMLDivElement>(null);
return (
<div className="relative w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 text-zinc-950 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50">
<div ref={scrollRef} className="relative h-[var(--demo-height,36rem)] overflow-y-auto overscroll-contain">
<header className="mx-auto max-w-2xl px-6 pt-16 pb-4 text-center sm:pt-20">
<p className="text-sm font-medium text-indigo-600 dark:text-indigo-400">Inside Northwind</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Everything your revenue team needs, in one window
</h2>
<p className="mt-4 text-zinc-600 dark:text-zinc-400">Scroll to take it apart.</p>
</header>
<ExplodedUI
walkthrough
containerRef={scrollRef}
stageClassName="h-[var(--demo-height,36rem)]"
className={cn("aspect-[16/10] w-full max-w-3xl", screen)}
>
<ExplodedUI.Layer label="App shell" description="Navigation, search and settings.">
<Shell />
</ExplodedUI.Layer>
<ExplodedUI.Layer
label="Sidebar"
description="Every workspace in one list."
className="top-[5.6%] bottom-0 left-0 w-[20%]"
>
<Sidebar />
</ExplodedUI.Layer>
<ExplodedUI.Layer
label="Live cards"
description="Metrics update in real time."
className="top-[17.5%] left-[23%] w-[74%]"
>
<MetricCards />
</ExplodedUI.Layer>
<ExplodedUI.Layer
label="Command menu"
description="Press ⌘K to do anything."
className="top-[36%] left-[40%] w-[38%]"
>
<CommandMenu />
</ExplodedUI.Layer>
<ExplodedUI.Layer
label="Live cursors"
description="See where your team is working."
bare
className="top-[58%] left-[66%] w-[11%]"
>
<Cursor />
</ExplodedUI.Layer>
</ExplodedUI>
<section className="mx-auto grid max-w-3xl gap-8 px-6 py-16 sm:grid-cols-3">
{[
["Built for speed", "Every screen loads in under 100ms, even with a million rows."],
["Keyboard first", "Anything you can click, you can do from the command menu."],
["Live by default", "Numbers, comments and cursors update the moment they change."],
].map(([title, body]) => (
<div key={title}>
<h3 className="font-medium">{title}</h3>
<p className="mt-2 text-sm leading-relaxed text-pretty text-zinc-600 dark:text-zinc-400">{body}</p>
</div>
))}
</section>
</div>
</div>
);
}
const nav = ["Overview", "Pipeline", "Accounts", "Reports"];
const workspaces = [
["Acme Corp", "bg-indigo-500"],
["Globex", "bg-emerald-500"],
["Initech", "bg-amber-500"],
["Umbrella", "bg-rose-500"],
];
function Shell() {
return (
<div className="flex size-full flex-col bg-white text-xs text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
<div className="flex h-[5.6%] shrink-0 items-center gap-1.5 border-b border-zinc-950/5 px-3 dark:border-white/5">
{[0, 1, 2].map((dot) => (
<span key={dot} className="size-2.5 rounded-full bg-zinc-200 dark:bg-zinc-700" />
))}
<span className="mx-auto rounded-md bg-zinc-100 px-3 py-0.5 text-zinc-500 dark:bg-zinc-800">
app.northwind.io/overview
</span>
</div>
<div className="flex min-h-0 flex-1">
{/* The sidebar's slot */}
<div className="w-[20%] shrink-0 bg-zinc-50 dark:bg-zinc-900" />
<div className="flex min-w-0 flex-1 flex-col gap-4 bg-zinc-50 p-5 dark:bg-zinc-950">
<div className="flex items-center justify-between">
<span className="text-lg font-semibold tracking-tight">Overview</span>
<div className="flex items-center gap-2 text-zinc-500">
<span className="flex w-40 items-center gap-2 rounded-md bg-white px-2.5 py-1.5 ring-1 ring-zinc-950/5 dark:bg-zinc-900 dark:ring-white/10">
<SearchIcon />
Search
<kbd className="ml-auto font-sans text-zinc-400">⌘K</kbd>
</span>
<span className="size-7 rounded-full bg-linear-to-br from-amber-300 to-rose-400" />
</div>
</div>
{/* The cards' slot */}
<div className="h-[22%] shrink-0" />
<div className="grid min-h-0 flex-1 grid-cols-[1.6fr_1fr] gap-4">
<div className="flex flex-col rounded-lg bg-white p-4 ring-1 ring-zinc-950/5 dark:bg-zinc-900 dark:ring-white/5">
<div className="flex justify-between text-zinc-500">
<span>Revenue</span>
<span>Last 12 weeks</span>
</div>
<div className="mt-4 flex min-h-0 flex-1 items-end gap-2">
{[38, 52, 44, 61, 57, 70, 64, 78, 72, 86, 81, 94].map((h, i) => (
<span
key={i}
className={cn("flex-1 rounded-sm", i === 11 ? "bg-indigo-500" : "bg-indigo-500/20 dark:bg-indigo-400/25")}
style={{ height: `${h}%` }}
/>
))}
</div>
</div>
<div className="rounded-lg bg-white p-4 ring-1 ring-zinc-950/5 dark:bg-zinc-900 dark:ring-white/5">
<span className="text-zinc-500">Top accounts</span>
<ul className="mt-3 space-y-3">
{workspaces.map(([name, color], i) => (
<li key={name} className="flex items-center gap-2">
<span className={cn("size-5 rounded-md", color)} />
<span className="font-medium">{name}</span>
<span className="ml-auto text-zinc-500 tabular-nums">${[48, 36, 22, 17][i]}k</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
);
}
function Sidebar() {
return (
<div className="flex size-full flex-col gap-0.5 border-r border-zinc-950/5 bg-white p-3 text-xs text-zinc-900 dark:border-white/5 dark:bg-zinc-900 dark:text-zinc-100">
<div className="mb-4 flex items-center gap-2 px-2 pt-1 text-sm font-semibold">
<span className="size-5 rounded-md bg-indigo-600" />
Northwind
</div>
{nav.map((item, i) => (
<span
key={item}
className={cn("rounded-md px-2 py-1.5", i === 0 ? "bg-zinc-100 font-medium dark:bg-zinc-800" : "text-zinc-500")}
>
{item}
</span>
))}
<span className="mt-5 mb-1 px-2 text-zinc-400">Workspaces</span>
{workspaces.map(([name, color]) => (
<span key={name} className="flex items-center gap-2 px-2 py-1.5 text-zinc-500">
<span className={cn("size-2 rounded-full", color)} />
{name}
</span>
))}
</div>
);
}
const metrics = [
{ label: "Revenue", value: "$48.2k", delta: "+12.4%", points: "0,30 15,26 30,28 45,18 60,20 75,10 90,12 100,4" },
{ label: "Active users", value: "2,931", delta: "+8.1%", points: "0,24 15,28 30,20 45,22 60,14 75,16 90,8 100,10" },
{ label: "Conversion", value: "3.8%", delta: "+0.6%", points: "0,28 15,22 30,24 45,20 60,22 75,14 90,16 100,8" },
];
function MetricCards() {
return (
<div className="grid grid-cols-3 gap-4 bg-zinc-50 text-xs text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
{metrics.map((metric) => (
<div key={metric.label} className="rounded-lg bg-white p-4 ring-1 ring-zinc-950/5 dark:bg-zinc-900 dark:ring-white/5">
<div className="flex items-center justify-between text-zinc-500">
{metric.label}
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
<span className="size-1.5 rounded-full bg-current" />
Live
</span>
</div>
<div className="mt-2 flex items-end justify-between gap-2">
<span className="text-2xl font-semibold tracking-tight tabular-nums">{metric.value}</span>
<svg viewBox="0 0 100 34" preserveAspectRatio="none" aria-hidden className="h-8 w-[45%] overflow-visible">
<polyline
points={metric.points}
fill="none"
stroke="currentColor"
strokeWidth="2"
vectorEffect="non-scaling-stroke"
className="text-indigo-500"
/>
</svg>
</div>
<span className="mt-1 block text-emerald-600 tabular-nums dark:text-emerald-400">{metric.delta} this week</span>
</div>
))}
</div>
);
}
const commands = [
{ label: "Create invoice", keys: "I" },
{ label: "Invite teammate", keys: "T" },
{ label: "Export report", keys: "E" },
{ label: "Switch workspace", keys: "W" },
];
function CommandMenu() {
return (
<div className="overflow-hidden rounded-lg bg-white text-xs text-zinc-900 shadow-2xl ring-1 shadow-zinc-950/20 ring-zinc-950/10 dark:bg-zinc-800 dark:text-zinc-100 dark:ring-white/10">
<div className="flex items-center gap-2 border-b border-zinc-950/5 px-3 py-2.5 text-sm text-zinc-500 dark:border-white/5">
<SearchIcon />
Type a command…
</div>
<div className="p-1.5">
<span className="block px-2 py-1 text-zinc-400">Suggestions</span>
{commands.map((command, i) => (
<span
key={command.label}
className={cn(
"flex items-center justify-between rounded-md px-2 py-1.5",
i === 1 && "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
)}
>
{command.label}
<kbd className="rounded bg-zinc-100 px-1.5 font-sans text-zinc-500 dark:bg-zinc-700 dark:text-zinc-300">
{command.keys}
</kbd>
</span>
))}
</div>
</div>
);
}
function Cursor() {
return (
<div className="flex flex-col items-start text-xs">
<svg viewBox="0 0 16 16" aria-hidden className="size-5 drop-shadow-md">
<path d="M2 1.5 14 7.2l-5.2 1.4L6.6 14Z" className="fill-rose-500 stroke-white" strokeWidth="1.2" strokeLinejoin="round" />
</svg>
<span className="mt-0.5 ml-3 rounded-full bg-rose-500 px-2 py-0.5 font-medium text-white shadow-md">Maya</span>
</div>
);
}
function SearchIcon() {
return (
<svg viewBox="0 0 16 16" aria-hidden className="size-3.5 shrink-0 fill-none stroke-current" strokeWidth="1.6">
<circle cx="7" cy="7" r="4.5" />
<path d="m10.5 10.5 3 3" strokeLinecap="round" />
</svg>
);
}
Live preview, examples and props: https://keyframeui.com/components/exploded-ui#exploded-ui-demo






















