About
A sleek, Dynamic-Island inspired stacked notification widget. Compresses multiple alerts into a layered 3D card deck that fans out smoothly on hover or click with individual dismiss actions.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Interactive Expandable Notification Stack to your project and fits it to your theme.
Code
How Interactive Expandable Notification Stack is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
// This is a file with a demo for your component
// That's what users will see in the preview
// Create new files in this directory to add more demos
import { useCallback, useRef, useState } from "react";
import { Bell, Plus, RotateCcw, Zap } from "lucide-react";
import { NotificationStack } from "@/components/ui/interactive-expandable-notification-stack";
import type { NotificationItem } from "@/components/ui/interactive-expandable-notification-stack";
/* ==========================================================================
Demo data
========================================================================== */
type NewNotification = Omit<NotificationItem, "id" | "timestamp">;
// Notifications the button picks from, one after another
const POOL: NewNotification[] = [
{ type: "success", title: "Payment received", description: "Olivia Martin paid $1,999 for the Nova Analytics Suite." },
{ type: "message", title: "New comment from Jackson", description: "“The updated pricing page looks great. Can we ship it this week?”" },
{ type: "info", title: "Deploy finished", description: "Version 2.4.1 is live on production. All checks passed." },
{ type: "warning", title: "Storage almost full", description: "You have used 92% of your 100 GB plan. Upgrade to avoid interruptions." },
{ type: "error", title: "Payment failed", description: "William Kim's card was declined. A retry is scheduled for tomorrow." },
{ type: "message", title: "Sofia mentioned you", description: "“@alex can you review the onboarding copy before lunch?”" },
{ type: "success", title: "Weekly report is ready", description: "Revenue is up 12.8% compared with last week." },
{ type: "info", title: "New sign-in detected", description: "Someone signed in from Lisbon on a new laptop. Was this you?" },
];
const INITIAL: NotificationItem[] = [
{ id: "seed-3", type: "info", title: "Welcome to your inbox", description: "Hover over the stack or click it to see every notification.", timestamp: "5 min ago" },
{ id: "seed-2", type: "message", title: "Maya shared a file", description: "Q4-roadmap.pdf is waiting for your review.", timestamp: "12 min ago" },
{ id: "seed-1", type: "success", title: "Backup complete", description: "All 248 projects were backed up safely.", timestamp: "1 hour ago" },
];
// end of demo data
/* ==========================================================================
Demo
========================================================================== */
// ONLY DEFAULT EXPORT WILL BE TREATED AS A DEMO
export default function DemoOne() {
const [notifications, setNotifications] = useState<NotificationItem[]>(INITIAL);
const [log, setLog] = useState("Nothing yet. Send a notification to get started.");
const nextId = useRef(1);
const latest = useRef(notifications); // the current list, readable from callbacks
latest.current = notifications;
const nextPool = useRef(0);
// Adds one notification to the top of the stack
const makeNotification = useCallback((): NotificationItem => {
const template = POOL[nextPool.current % POOL.length];
nextPool.current += 1;
const id = `demo-${nextId.current}`;
nextId.current += 1;
return { ...template, id, timestamp: "Just now" };
}, []);
const sendOne = () => {
const item = makeNotification();
setNotifications((list) => [item, ...list].slice(0, 12));
setLog(`Sent: ${item.title}`);
};
const sendThree = () => {
const items = [makeNotification(), makeNotification(), makeNotification()];
setNotifications((list) => [...items.reverse(), ...list].slice(0, 12));
setLog("Sent 3 notifications at once.");
};
const handleDismiss = useCallback((id: string) => {
const gone = latest.current.find((n) => n.id === id);
if (gone) setLog(`Dismissed: ${gone.title}`);
setNotifications((list) => list.filter((n) => n.id !== id));
}, []);
const handleClearAll = useCallback(() => {
setNotifications([]);
setLog("Cleared all notifications.");
}, []);
const reset = () => {
setNotifications(INITIAL);
setLog("Back to the starting set.");
};
return (
<div className="flex min-h-screen w-full items-center justify-center bg-background px-4 py-12 text-foreground sm:px-6">
<div className="grid w-full max-w-4xl items-center gap-12 md:grid-cols-2">
{/* Controls */}
<div>
<span className="inline-flex h-11 w-11 items-center justify-center rounded-xl border border-border bg-card text-primary shadow-sm">
<Bell size={20} aria-hidden="true" />
</span>
<h1 className="mt-5 text-3xl font-semibold leading-tight tracking-tight sm:text-4xl">Notifications that stay out of your way</h1>
<p className="mt-3 max-w-md text-base leading-relaxed text-muted-foreground">
Everything arrives in one tidy stack. Open it when you want to read, dismiss what you have dealt with and clear the rest in one go.
</p>
<div className="mt-7 flex flex-wrap gap-3">
<button
type="button"
onClick={sendOne}
className="inline-flex h-11 items-center gap-2 rounded-xl bg-primary px-5 text-sm font-medium text-primary-foreground outline-none transition-all hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.98]"
>
<Plus size={16} aria-hidden="true" />
Send a notification
</button>
<button
type="button"
onClick={sendThree}
className="inline-flex h-11 items-center gap-2 rounded-xl border border-border bg-card px-5 text-sm font-medium outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.98]"
>
<Zap size={16} aria-hidden="true" />
Send three
</button>
<button
type="button"
onClick={reset}
className="inline-flex h-11 items-center gap-2 rounded-xl px-4 text-sm font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
<RotateCcw size={15} aria-hidden="true" />
Reset
</button>
</div>
<ul className="mt-8 space-y-2 text-sm text-muted-foreground">
<li>Hover or click the stack to open it.</li>
<li>Swipe a card sideways, or press its X, to dismiss it.</li>
<li>Press Escape to close a stack you opened with a click.</li>
</ul>
<p className="mt-6 rounded-lg border border-border bg-muted/50 px-3 py-2 text-xs text-muted-foreground" role="status" aria-live="polite">
Last event: {log}
</p>
</div>
{/* The stack */}
<div className="flex min-h-[420px] justify-center rounded-3xl border border-border bg-muted/40 p-6 sm:p-8">
<NotificationStack notifications={notifications} onDismiss={handleDismiss} onClearAll={handleClearAll} title="Inbox" emptyMessage="You're all caught up. Send a new notification to see it arrive." />
</div>
</div>
</div>
);
}
// end of demo








