Avatar Stack

Overlapping avatars that lift and reveal a tooltip on hover.

AC
LP
NK
MR
EB
+12

Joined by 2,000+ designers this month

Installation

  1. Install the dependencies

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

    components/ui/avatar-stack.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { useState } from "react";
    
    type Person = { name: string; role: string };
    
    const people: Person[] = [
      { name: "Ava Chen", role: "Design" },
      { name: "Liam Patel", role: "Engineering" },
      { name: "Noah Kim", role: "Product" },
      { name: "Mia Rossi", role: "Marketing" },
      { name: "Ethan Brooks", role: "Sales" },
    ];
    
    const tones = ["bg-blue-600", "bg-violet-600", "bg-emerald-600", "bg-amber-500", "bg-rose-500"];
    
    export function AvatarStack({ items = people, extra = 12 }: { items?: Person[]; extra?: number }) {
      const [hovered, setHovered] = useState<number | null>(null);
    
      return (
        <div className="flex items-center">
          {items.map((p, i) => (
            <div
              key={p.name}
              className="relative -ml-3 first:ml-0"
              onMouseEnter={() => setHovered(i)}
              onMouseLeave={() => setHovered(null)}
            >
              <AnimatePresence>
                {hovered === i && (
                  <motion.div
                    initial={{ opacity: 0, y: 8, scale: 0.9 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={{ opacity: 0, y: 4, scale: 0.95 }}
                    className="absolute -top-14 left-1/2 z-30 -translate-x-1/2 whitespace-nowrap rounded-lg bg-zinc-900 px-3 py-1.5 text-center shadow-lg"
                  >
                    <p className="text-xs font-medium text-white">{p.name}</p>
                    <p className="text-[10px] text-zinc-400">{p.role}</p>
                  </motion.div>
                )}
              </AnimatePresence>
              <motion.span
                whileHover={{ y: -6, scale: 1.08 }}
                transition={{ type: "spring", stiffness: 400, damping: 18 }}
                style={{ zIndex: hovered === i ? 20 : items.length - i }}
                className={`relative grid size-11 cursor-pointer place-items-center rounded-full text-sm font-semibold text-white ring-4 ring-white ${tones[i % tones.length]}`}
              >
                {p.name.split(" ").map((n) => n[0]).join("")}
              </motion.span>
            </div>
          ))}
          {extra > 0 && (
            <span className="relative -ml-3 grid size-11 place-items-center rounded-full bg-zinc-100 text-xs font-semibold text-zinc-600 ring-4 ring-white">
              +{extra}
            </span>
          )}
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <div className="flex flex-col items-center gap-4 pt-10">
          <AvatarStack />
          <p className="text-sm text-zinc-500">
            Joined by <span className="font-medium text-zinc-900">2,000+</span> designers this month
          </p>
        </div>
      );
    }
    
  3. Use it

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