Purbayan Pramanik

Notification Stack

Component 22

About

A persistent notification centre whose stacked items collapse into a pile with peeking edges, fanning apart when pulled and closing again with drag-to-dismiss cards.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Notification Stack to your project and fits it to your theme.

Code

How Notification Stack is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import {
  NotificationStack,
  type NotificationItem,
} from "@/components/ui/notification-stack";
import { useState } from "react";
import {
  AlertTriangle,
  CalendarClock,
  CreditCard,
  GitPullRequest,
  Mail,
  MessageSquare,
  Package,
  ServerCog,
  ShieldCheck,
  UserPlus,
} from "lucide-react";

import { Button } from "@/components/ui/button";

type Draft = Omit<NotificationItem, "id" | "meta">;

/**
 * Ten of them, cycled, rather than one message with a rising build number. A
 * pile of four identical cards says nothing about how the component handles
 * notifications that differ in source and in what they have to say.
 *
 * Every body is trimmed to one line. That is presentation for these pages, not
 * a constraint of the component. `body` clamps at two, and the pile solves each
 * peek from the bottom edge up so cards of different heights still come out
 * even. Uniform cards don't exercise it.
 */
const drafts: Draft[] = [
  {
    icon: <GitPullRequest />,
    title: "Review requested",
    body: "danabramov opened “Drop the scheduler”.",
  },
  {
    icon: <ServerCog />,
    title: "Deploy finished",
    body: "Build 1841 shipped in 2m 11s.",
  },
  {
    icon: <Mail />,
    title: "Invoice #4021 paid",
    body: "Northwind Trading, $12,480 by ACH.",
  },
  {
    icon: <AlertTriangle />,
    title: "p99 latency above 800ms",
    body: "api-gateway, eu-west-1.",
  },
  {
    icon: <MessageSquare />,
    title: "Priya Raman",
    body: "Migration is behind a flag.",
  },
  {
    icon: <UserPlus />,
    title: "Marco Reyes joined the workspace",
    body: "Invited by Priya Raman.",
  },
  {
    icon: <CalendarClock />,
    title: "Design review",
    body: "Starts in 15 minutes.",
  },
  {
    icon: <ShieldCheck />,
    title: "Access review due",
    body: "Nine roles need attesting.",
  },
  {
    icon: <CreditCard />,
    title: "Card ending 4417 expires this month",
    body: "No fallback method on file.",
  },
  {
    icon: <Package />,
    title: "v2.4.0 published",
    body: "3 packages, 1.2MB unpacked.",
  },
];

/** Newest first. Everything below the top shifts one step down the list as
 *  arrivals push it back, so a pile of four doesn't read "now" four times. */
const ages = ["now", "1m", "4m", "12m", "38m", "1h"];

function withAges(items: (Draft & { id: string })[]): NotificationItem[] {
  return items.map((item, index) => ({
    ...item,
    meta: ages[Math.min(index, ages.length - 1)],
  }));
}

/** A real centre trims on a schedule; this one trims so the demo stays inside
 *  the room the preview reserves for it. */
const CAP = 4;

const seed = withAges(
  drafts
    .slice(0, CAP)
    .map((draft, index) => ({ ...draft, id: `seed-${index}` })),
);

let arrivals = 0;

export default function NotificationStackBasicExample() {
  const [items, setItems] = useState(seed);

  const add = () => {
    const draft = drafts[arrivals % drafts.length];
    arrivals += 1;
    setItems((current) =>
      withAges(
        [{ ...draft, id: `arrival-${arrivals}` }, ...current].slice(0, CAP),
      ),
    );
  };

  return (
    <div className="flex w-full max-w-sm flex-col gap-5">
      {/* Fixed reserve, pile centred in it. The stack's own height animates, so
          without this the whole docs page reflows every time it opens. */}
      <div className="flex h-[308px] items-center">
        <NotificationStack
          className="w-full"
          notifications={items}
          onDismiss={(id) =>
            setItems((current) => current.filter((item) => item.id !== id))
          }
        />
      </div>
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={add}>
          New notification
        </Button>
        <Button variant="ghost" size="sm" onClick={() => setItems(seed)}>
          Reset
        </Button>
      </div>
    </div>
  );
}

Dependencies

More by Purbayan Pramanik

View all
  • Proximity Accordion

    Proximity Accordion

    5
  • Plan Checklist

    Plan Checklist

  • Agent Activity

    Agent Activity

  • Snap Panels

    Snap Panels

  • Checkbox Group

    Checkbox Group

  • Conversation

    Conversation