Skip to content
All Taste components

Animated app UI

Notification stack

An inbox of updates that slide in one at a time, with the unread count popping in once they’ve all arrived.

4 notifications · spring slide-in · unread count · 4 tones

4Notifications
Payment receivedKestrel Studio paid ₹48,200 for INV-1042.
Invoice viewedBrightwater Design opened INV-1043.
Due tomorrowINV-1039 to Oakline Joinery, ₹12,000.
Asha joined your teamShe can now create and send invoices.
import { Bell, Check, Clock, Mail, Users } from "lucide-react";
import { cn } from "@/lib/utils";

/* Add once to globals.css:
@keyframes toast-in { from { opacity: 0; transform: translateX(44px) scale(.95); } 55% { opacity: 1; } }
*/

const notifications = [
  { icon: Check, tone: "bg-success-soft text-success-fg", title: "Payment received", text: "Kestrel Studio paid ₹48,200 for INV-1042.", time: "now" },
  { icon: Mail, tone: "bg-info-soft text-info-fg", title: "Invoice viewed", text: "Brightwater Design opened INV-1043.", time: "4m" },
  { icon: Clock, tone: "bg-warning-soft text-warning-fg", title: "Due tomorrow", text: "INV-1039 to Oakline Joinery, ₹12,000.", time: "1h" },
  { icon: Users, tone: "bg-primary-soft text-primary-soft-fg", title: "Asha joined your team", text: "She can now create and send invoices.", time: "3h" },
];
const STEP = 380; // ms between arrivals

export function NotificationStack() {
  return (
    <section aria-label="Notifications" className="mx-auto max-w-[400px] overflow-hidden rounded-lg border border-border bg-bg font-sans shadow-[0_18px_40px_-26px_rgb(0_0_0/.35)]">
      <header className="flex items-center gap-2.5 border-b border-border px-3.5 py-3">
        <span className="relative grid size-8 place-items-center rounded bg-surface text-fg-muted">
          <Bell className="size-4" aria-hidden />
          <span
            className="absolute -right-1.5 -top-1.5 grid h-[18px] min-w-[18px] place-items-center rounded-full border-2 border-bg bg-primary px-1 text-[10.5px] font-semibold text-primary-fg animate-pop motion-reduce:animate-none"
            style={{ animationDelay: 250 + notifications.length * STEP + 100 + "ms" }}
          >
            {notifications.length}
          </span>
        </span>
        <h2 className="flex-1 text-[15px] font-medium text-fg">Notifications</h2>
        <button type="button" className="h-8 rounded px-2.5 text-[13px] font-medium text-fg transition hover:bg-surface-2">Mark all read</button>
      </header>
      <ul className="grid gap-2.5 bg-surface p-3.5">
        {notifications.map((n, i) => (
          <li
            key={n.title}
            className="flex items-start gap-3 rounded-lg border border-border bg-bg p-3 text-[13px] shadow-[0_10px_28px_-20px_rgb(0_0_0/.45)] animate-[toast-in_.7s_cubic-bezier(.34,1.56,.64,1)_both] motion-reduce:animate-none"
            style={{ animationDelay: 250 + i * STEP + "ms" }}
          >
            <span className={cn("grid size-8 shrink-0 place-items-center rounded", n.tone)}><n.icon className="size-4" aria-hidden /></span>
            <div className="min-w-0 flex-1">
              <p className="font-medium text-fg">{n.title}</p>
              <p className="text-fg-muted">{n.text}</p>
            </div>
            <time className="shrink-0 text-[11.5px] text-fg-subtle">{n.time}</time>
          </li>
        ))}
      </ul>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.