# Avatar Stack Expand — React Avatar Component

> An overlapping avatar stack that springs apart on hover to show first names, with a +N chip that deals out the rest.

- Category: [Avatars](https://pixelpapa.com/components/avatars.md)
- Tags: avatar, stack, hover, expand, group
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/avatars/avatar-stack-expand

## Install

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

The file is written to `components/pixelpapa/avatar-stack-expand.tsx`. To install manually, run `npm install 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 { AnimatePresence, motion } from "motion/react";
import { useState, type FocusEvent } from "react";
import { cn } from "@/lib/utils";

type Person = { name: string; initials: string; hue: number };

type AvatarStackProps = {
  people: Person[];
  /** How many faces show before the +N chip. */
  max?: number;
  size?: number;
  className?: string;
};

const spring = { type: "spring", stiffness: 380, damping: 26 } as const;

export function AvatarStack({ people, max = 5, size = 48, className }: AvatarStackProps) {
  const [spread, setSpread] = useState(false);
  const [all, setAll] = useState(false);
  const shown = all ? people : people.slice(0, max);
  const extra = people.length - max;

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

  return (
    <div
      onPointerEnter={() => setSpread(true)}
      onPointerLeave={() => setSpread(false)}
      onFocus={() => setSpread(true)}
      onBlur={onBlur}
      className={cn("flex items-start pb-6", className)}
    >
      <AnimatePresence initial={false} mode="popLayout">
        {shown.map((p, i) => (
          <motion.div
            key={p.name}
            layout
            title={p.name}
            className="relative shrink-0"
            style={{ zIndex: shown.length - i }}
            initial={{ opacity: 0, scale: 0.4 }}
            animate={{ opacity: 1, scale: 1, marginLeft: i === 0 ? 0 : spread ? 8 : -size * 0.3 }}
            exit={{ opacity: 0, scale: 0.4 }}
            transition={{ ...spring, delay: i * 0.015 }}
          >
            <span
              className="flex items-center justify-center rounded-full font-semibold text-white ring-[3px] ring-card"
              style={{ width: size, height: size, fontSize: size * 0.34, background: `linear-gradient(135deg, oklch(0.78 0.14 ${p.hue}), oklch(0.54 0.2 ${p.hue + 40}))` }}
            >
              {p.initials}
            </span>
            <motion.span
              aria-hidden
              className="absolute top-full left-1/2 mt-1.5 -translate-x-1/2 text-[11px] whitespace-nowrap text-muted-foreground"
              animate={{ opacity: spread ? 1 : 0, y: spread ? 0 : -6 }}
              transition={{ duration: 0.2, delay: spread ? 0.05 + i * 0.02 : 0 }}
            >
              {p.name.split(" ")[0]}
            </motion.span>
          </motion.div>
        ))}
      </AnimatePresence>
      {extra > 0 && (
        <motion.button
          layout
          type="button"
          aria-expanded={all}
          aria-label={all ? "Show fewer people" : `Show ${extra} more people`}
          onClick={() => setAll((a) => !a)}
          animate={{ marginLeft: spread ? 8 : -size * 0.3 }}
          transition={spring}
          whileTap={{ scale: 0.9 }}
          className="relative z-0 flex shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium text-foreground ring-[3px] ring-card outline-none hover:bg-accent focus-visible:ring-ring"
          style={{ width: size, height: size }}
        >
          {all ? "−" : `+${extra}`}
        </motion.button>
      )}
    </div>
  );
}

const crew: Person[] = [
  { name: "Noor Haddad", initials: "NH", hue: 262 },
  { name: "Jonas Lind", initials: "JL", hue: 25 },
  { name: "Mei Tanaka", initials: "MT", hue: 330 },
  { name: "Kwame Osei", initials: "KO", hue: 150 },
  { name: "Elsa Brandt", initials: "EB", hue: 200 },
  { name: "Rafa Molina", initials: "RM", hue: 75 },
  { name: "Ida Sorensen", initials: "IS", hue: 295 },
  { name: "Theo Laurent", initials: "TL", hue: 110 },
];

export default function AvatarStackExpandDemo() {
  return (
    <div className="w-full max-w-xl rounded-[28px] border border-border bg-card p-7 shadow-xl shadow-primary/5">
      <p className="text-xs font-medium tracking-[0.18em] text-primary uppercase">Thursday · 10:00</p>
      <h3 className="mt-2 text-xl font-semibold tracking-tight text-card-foreground">Design crit: onboarding v3</h3>
      <p className="mt-1 text-sm text-muted-foreground">Room Juniper and on video · {crew.length} going</p>
      <div className="mt-6 flex min-h-[76px] justify-start">
        <AvatarStack people={crew} />
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Avatar Stack Expand" component from PixelPapa UI to this project.

What it is: An overlapping avatar stack that springs apart on hover to show first names, with a +N chip that deals out the rest.
Category: Avatars. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/avatar-stack-expand.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/avatar-stack-expand.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/avatar-stack-expand" 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 avatars

- [Avatar Group Tooltip Names](https://pixelpapa.com/components/avatars/avatar-group-tooltip-names.md): An avatar row sharing one tooltip that glides from face to face on a spring while the hovered avatar lifts and its neighbours step aside.
- [Avatar Hover Card](https://pixelpapa.com/components/avatars/avatar-hover-card.md) (Pro): An avatar that opens a profile popover on hover or focus, scaling out of the trigger on a spring with forgiving open and close delays.
- [Avatar Status Ring](https://pixelpapa.com/components/avatars/avatar-status-ring.md): An avatar wrapped in a presence ring: online radiates soft pulses, away dims to amber, busy holds a solid red, and the badge pops when status changes.
- [Avatar Story Ring](https://pixelpapa.com/components/avatars/avatar-story-ring.md) (Pro): Story avatars with a gradient ring that breaks into spinning dashes while the story loads, then settles into a quiet seen state.
- [Avatar Upload Dropzone](https://pixelpapa.com/components/avatars/avatar-upload-dropzone.md) (Pro): A circular avatar dropzone: its dashed ring spins and swells when a file hovers over it, then the photo drops in over the initials fallback.
