# Card Notification Stack — React Card Component

> A loose pile of notifications that straightens and fans out from its centre into a readable list on hover or focus.

- Category: [Cards](https://pixelpapa.com/components/cards.md)
- Tags: card, notification, stack, hover, spring
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/cards/card-notification-stack

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/card-notification-stack.json"
```

The file is written to `components/pixelpapa/card-notification-stack.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Calendar, CreditCard, GitBranch, MessageCircle, type LucideIcon } from "lucide-react";
import { motion } from "motion/react";
import { useState, type FocusEvent } from "react";
import { cn } from "@/lib/utils";

type StackItem = { id: string; app: string; title: string; body: string; time: string; icon: LucideIcon; tint: string };

type NotificationStackProps = {
  items: StackItem[];
  className?: string;
  /** Height of one card in px. */
  itemHeight?: number;
  gap?: number;
};

const tilt = [0, -2.5, 2, -1.5];

export function NotificationStack({ items, className, itemHeight = 78, gap = 10 }: NotificationStackProps) {
  const [open, setOpen] = useState(false);
  const n = items.length;
  const full = n * itemHeight + (n - 1) * gap;
  const mid = (full - itemHeight) / 2;

  function onBlur(e: FocusEvent<HTMLDivElement>) {
    if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
  }

  return (
    // The box keeps its expanded height so nothing around it moves; cards spread from the middle.
    <div
      role="list"
      aria-label="Notifications"
      onPointerEnter={() => setOpen(true)}
      onPointerLeave={() => setOpen(false)}
      onFocus={() => setOpen(true)}
      onBlur={onBlur}
      className={cn("relative w-full max-w-sm", className)}
      style={{ height: full }}
    >
      {items.map((item, i) => {
        const Icon = item.icon;
        const peek = Math.min(i, 3);
        return (
          <motion.div
            key={item.id}
            role="listitem"
            className="absolute inset-x-0 top-0"
            style={{ height: itemHeight, zIndex: n - i }}
            initial={false}
            animate={
              open
                ? { y: i * (itemHeight + gap), scale: 1, rotate: 0, opacity: 1 }
                : { y: mid + peek * 9, scale: 1 - peek * 0.045, rotate: tilt[peek], opacity: i > 2 ? 0 : 1 }
            }
            transition={{ type: "spring", stiffness: 340, damping: 28, delay: open ? i * 0.025 : (n - i) * 0.02 }}
          >
            <button
              type="button"
              tabIndex={open || i === 0 ? 0 : -1}
              className="flex size-full items-center gap-3.5 rounded-2xl border border-border bg-card px-4 text-left shadow-lg shadow-black/[0.06] outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring dark:shadow-black/30"
            >
              <span className="flex size-10 shrink-0 items-center justify-center rounded-xl text-white" style={{ background: item.tint }}>
                <Icon className="size-5" />
              </span>
              <span className={cn("min-w-0 flex-1 transition-opacity duration-200", !open && i > 0 && "opacity-0")}>
                <span className="flex items-baseline justify-between gap-3">
                  <span className="truncate text-sm font-medium text-card-foreground">{item.title}</span>
                  <span className="shrink-0 text-[11px] text-muted-foreground">{item.time}</span>
                </span>
                <span className="mt-0.5 block truncate text-[13px] text-muted-foreground">{item.body}</span>
              </span>
            </button>
          </motion.div>
        );
      })}
      <motion.p
        aria-hidden
        animate={{ opacity: open ? 0 : 1 }}
        className="pointer-events-none absolute inset-x-0 text-center text-xs text-muted-foreground"
        style={{ top: mid + itemHeight + 34 }}
      >
        {n} notifications · hover to expand
      </motion.p>
    </div>
  );
}

const notifications: StackItem[] = [
  { id: "1", app: "Messages", title: "Priya Nair", body: "Pushed the new onboarding copy, can you take a look?", time: "now", icon: MessageCircle, tint: "oklch(0.62 0.19 150)" },
  { id: "2", app: "Calendar", title: "Design review in 10 min", body: "Room Juniper · with Malik and 3 others", time: "2m", icon: Calendar, tint: "oklch(0.62 0.21 25)" },
  { id: "3", app: "Billing", title: "Payment received", body: "$1,240.00 from Harbor Studio, invoice #0193", time: "18m", icon: CreditCard, tint: "oklch(0.55 0.22 265)" },
  { id: "4", app: "Deploys", title: "Preview ready", body: "feat/saved-filters deployed in 41 seconds", time: "1h", icon: GitBranch, tint: "oklch(0.5 0.03 265)" },
];

export default function CardNotificationStackDemo() {
  return <NotificationStack items={notifications} />;
}
```

## AI prompt

```text
Add the "Card Notification Stack" component from PixelPapa UI to this project.

What it is: A loose pile of notifications that straightens and fans out from its centre into a readable list on hover or focus.
Category: Cards. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/card-notification-stack.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/card-notification-stack.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/card-notification-stack" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More cards

- [Card 3D Tilt](https://pixelpapa.com/components/cards/card-3d-tilt.md) (Pro): A card that leans toward the pointer on springs, with a glare that tracks the cursor and layers floating at different depths.
- [Card Border Beam](https://pixelpapa.com/components/cards/card-border-beam.md) (Pro): A card with a comet of light travelling around its border, drawn with an animated conic gradient and a soft glow copy.
- [Card Expandable Detail](https://pixelpapa.com/components/cards/card-expandable-detail.md) (Pro): Compact rows that expand in place with a shared layout animation, pushing their neighbours aside on a spring.
- [Card Flip](https://pixelpapa.com/components/cards/card-flip.md) (Pro): A two-sided card that turns over on a spring when clicked or activated from the keyboard, on either axis.
- [Card Glass Frost](https://pixelpapa.com/components/cards/card-glass-frost.md): A frosted glass card with a lit top edge and fine grain, floating over slowly drifting colour blobs.
- [Card Hover Reveal Actions](https://pixelpapa.com/components/cards/card-hover-reveal-actions.md) (Pro): A media card whose artwork zooms on hover while quick actions spring in from the edge and a call to action rises from the footer.
