Skip to content
All Taste components

Animated screens

Notification stack

A lock screen where notifications arrive, stack on top of each other and the newest one opens.

fan-in arrivals · stacked cards · expand with actions

Friday, 16 October

9:41

Basket

Out for delivery

Your groceries arrive by 10:15 am.

Calendar

Design review in 15 min

Room 4B · with Meera, Jon

Messages

Ananya Bose

Are we still on for dinner at 8? I booked a table at Toast & Tonic.

/* globals.css
@keyframes fan-in { from { opacity: 0; transform: translateY(80px) scale(.9) rotate(-4deg); } }
*/
"use client";

import { useState } from "react";
import { Calendar, Camera, Flashlight, Lock, MessageCircle, Package } from "lucide-react";
import { cn } from "@/lib/utils";

type Note = { id: string; app: string; time: string; title: string; body: string; icon: typeof Package; tone: string };

// Newest last: it lands on top of the stack.
const notes: Note[] = [
  { id: "n1", app: "Basket", time: "32m ago", title: "Out for delivery", body: "Your groceries arrive by 10:15 am.", icon: Package, tone: "bg-warning" },
  { id: "n2", app: "Calendar", time: "9:30 am", title: "Design review in 15 min", body: "Room 4B · with Meera, Jon", icon: Calendar, tone: "bg-danger" },
  { id: "n3", app: "Messages", time: "now", title: "Ananya Bose", body: "Are we still on for dinner at 8? I booked a table at Toast & Tonic.", icon: MessageCircle, tone: "bg-success" },
];

export default function LockScreen() {
  const [open, setOpen] = useState(false);
  const EXPAND = 66; // px the top card grows by

  return (
    <div className="relative mx-auto h-dvh max-w-md overflow-hidden bg-[radial-gradient(120%_70%_at_0%_0%,var(--tst-brand-400),transparent_60%),radial-gradient(100%_70%_at_100%_100%,var(--tst-info-500),transparent_60%),linear-gradient(170deg,var(--tst-brand-700),var(--tst-brand-950)_70%,var(--tst-neutral-950))] font-sans text-neutral-50">
      <Lock className="mx-auto mt-14 size-4" aria-hidden />
      <p className="mt-4 text-center text-[15px] font-medium opacity-90">Friday, 16 October</p>
      <p className="text-center text-[76px] font-semibold leading-none tracking-tighter tabular-nums">9:41</p>

      <ul aria-label="Notifications" className="relative mx-2 mt-6 h-60">
        {notes.map((n, i) => {
          const depth = notes.length - 1 - i; // 0 = top card
          const Icon = n.icon;
          const y = depth * 11 + (depth > 0 && open ? EXPAND : 0);
          return (
            <li key={n.id} style={{ transform: "translateY(" + y + "px) scale(" + (1 - depth * 0.06) + ")", opacity: 1 - depth * 0.12, zIndex: 10 - depth, animationDelay: i * 450 + "ms" }}
              className="absolute inset-x-0 top-0 origin-bottom animate-[fan-in_.6s_cubic-bezier(.2,.7,.1,1)_both] transition duration-500 ease-taste motion-reduce:animate-none motion-reduce:transition-none">
              <button type="button" disabled={depth > 0} onClick={() => setOpen(!open)} aria-expanded={depth === 0 ? open : undefined}
                className="w-full rounded-[22px] bg-neutral-50/90 px-3 py-2.5 text-left text-neutral-950 shadow-[0_10px_30px_-14px_rgb(0_0_0/.45)] backdrop-blur-xl">
                <div className="flex gap-2.5">
                  <span className={cn("grid size-[34px] shrink-0 place-items-center rounded-[9px] text-neutral-50", n.tone)}><Icon className="size-[18px]" aria-hidden /></span>
                  <div className="min-w-0 flex-1">
                    <div className="flex justify-between text-[11.5px] text-neutral-600"><b className="font-semibold uppercase tracking-wide">{n.app}</b><time>{n.time}</time></div>
                    <p className="text-[13.5px] font-semibold leading-[18px]">{n.title}</p>
                    <p className={cn("text-[13px] leading-[18px] text-neutral-700", !(open && depth === 0) && "line-clamp-1")}>{n.body}</p>
                  </div>
                </div>
                {depth === 0 && (
                  <div className={cn("grid transition-[grid-template-rows,opacity] duration-500 ease-taste", open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0")}>
                    <div className="overflow-hidden">
                      <div className="mt-2.5 grid grid-cols-2 gap-2 border-t border-neutral-200 pt-2.5 text-[13px] font-medium">
                        <span className="rounded-full bg-neutral-200 py-2 text-center">Reply</span>
                        <span className="rounded-full bg-neutral-200 py-2 text-center">Mark as read</span>
                      </div>
                    </div>
                  </div>
                )}
              </button>
            </li>
          );
        })}
      </ul>

      <div className="absolute inset-x-7 bottom-8 flex justify-between">
        <button type="button" aria-label="Torch" className="grid size-[46px] place-items-center rounded-full bg-neutral-950/35 backdrop-blur"><Flashlight className="size-5" /></button>
        <button type="button" aria-label="Camera" className="grid size-[46px] place-items-center rounded-full bg-neutral-950/35 backdrop-blur"><Camera className="size-5" /></button>
      </div>
    </div>
  );
}

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