Animated List

Live notifications that slide in at the top of a stack.

    Installation

    1. Install the dependencies

      $ npm i motion lucide-react
    2. Copy the source into components/ui/animated-list.tsx

      components/ui/animated-list.tsx
      "use client";
      
      import { AnimatePresence, motion } from "motion/react";
      import { CreditCard, MessageCircle, UserPlus, Zap, type LucideIcon } from "lucide-react";
      import { useEffect, useState } from "react";
      
      type Notice = { title: string; text: string; time: string; icon: LucideIcon; tone: string };
      
      const notices: Notice[] = [
        { title: "Payment received", text: "$2,400 from Lumora Inc.", time: "now", icon: CreditCard, tone: "bg-emerald-100 text-emerald-600" },
        { title: "New signup", text: "Priya joined your workspace", time: "1m", icon: UserPlus, tone: "bg-blue-100 text-blue-600" },
        { title: "New message", text: "Can we move the call to 3pm?", time: "2m", icon: MessageCircle, tone: "bg-violet-100 text-violet-600" },
        { title: "Deploy finished", text: "Production is live in 41s", time: "4m", icon: Zap, tone: "bg-amber-100 text-amber-600" },
      ];
      
      type AnimatedListProps = {
        items?: Notice[];
        /** Milliseconds between new items */
        interval?: number;
        /** Items visible at once */
        max?: number;
      };
      
      export function AnimatedList({ items = notices, interval = 1800, max = 4 }: AnimatedListProps) {
        const [list, setList] = useState<(Notice & { uid: number })[]>([]);
      
        useEffect(() => {
          let n = 0;
          const add = () => {
            const uid = n;
            setList((prev) => [{ ...items[uid % items.length], uid }, ...prev].slice(0, max));
            n++;
          };
          add();
          const id = setInterval(add, interval);
          return () => clearInterval(id);
        }, [items, interval, max]);
      
        return (
          <ul className="flex w-full flex-col gap-2.5">
            <AnimatePresence initial={false}>
              {list.map((item) => (
                <motion.li
                  key={item.uid}
                  layout
                  initial={{ opacity: 0, y: -24, scale: 0.95 }}
                  animate={{ opacity: 1, y: 0, scale: 1 }}
                  exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.2 } }}
                  transition={{ type: "spring", stiffness: 350, damping: 30 }}
                  className="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white p-3.5 shadow-sm"
                >
                  <span className={`grid size-10 shrink-0 place-items-center rounded-xl ${item.tone}`}>
                    <item.icon className="size-5" />
                  </span>
                  <div className="min-w-0 flex-1">
                    <p className="text-sm font-medium text-zinc-900">{item.title}</p>
                    <p className="truncate text-sm text-zinc-500">{item.text}</p>
                  </div>
                  <span className="text-xs text-zinc-400">{item.time}</span>
                </motion.li>
              ))}
            </AnimatePresence>
          </ul>
        );
      }
      
      export default function Demo() {
        return (
          <div className="h-[340px] w-full max-w-sm overflow-hidden">
            <AnimatedList />
          </div>
        );
      }
      
    3. Use it

      The file exports the component and a Demo showing typical usage. Import whichever you need.