# Avatar Group

Overlapping avatars that fan apart on hover, with names that spring up above each one.

**Category:** Display  
**Exports:** `AvatarGroup`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/avatar-group.json
```
The file lands at `components/ui/avatar-group.tsx`; import from `@/components/ui/avatar-group`.

## API — Avatar Group
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `people` | `{ name: string; src?: string }[]` | — |  |
| `max` | `number` | `5` |  |
| `size` | `number` | `40` |  |

## Example — Avatar Group

```tsx
import { AvatarGroup } from "@/components/ui/avatar-group";

export default function AvatarGroupDemo({ max = 5, size = 40 }: { max?: number; size?: number }) {
  return (
    <div className="pt-8">
      <AvatarGroup
        max={max}
        size={size}
        people={[
          { name: "Ana Souza" },
          { name: "Ravi Patel" },
          { name: "Mei Tanaka" },
          { name: "Omar Haddad" },
          { name: "Lena Fischer" },
          { name: "Sam Okafor" },
          { name: "Iris Novak" },
        ]}
      />
    </div>
  );
}
```

## Source — components/ui/avatar-group.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type Person = { name: string; src?: string };

export type AvatarGroupProps = {
  people: Person[];
  max?: number;
  size?: number;
  className?: string;
};

const initials = (name: string) =>
  name
    .split(/\s+/)
    .map((p) => p[0])
    .slice(0, 2)
    .join("")
    .toUpperCase();

export function AvatarGroup({ people, max = 5, size = 40, className }: AvatarGroupProps) {
  const reduce = useReducedMotion();
  const [spread, setSpread] = React.useState(false);
  const [hovered, setHovered] = React.useState<number | null>(null);
  const shown = people.slice(0, max);
  const extra = people.length - shown.length;
  const overlap = size * 0.32;
  const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 420, damping: 28 };

  return (
    <div
      role="group"
      aria-label={`${people.length} people`}
      className={cn("flex items-center", className)}
      onPointerEnter={() => setSpread(true)}
      onPointerLeave={() => {
        setSpread(false);
        setHovered(null);
      }}
    >
      {shown.map((p, i) => (
        <motion.div
          key={p.name}
          tabIndex={0}
          aria-label={p.name}
          onPointerEnter={() => setHovered(i)}
          onFocus={() => {
            setSpread(true);
            setHovered(i);
          }}
          onBlur={() => setHovered(null)}
          animate={{ marginLeft: i === 0 ? 0 : spread ? 4 : -overlap, y: hovered === i && !reduce ? -4 : 0 }}
          transition={spring}
          className="relative shrink-0 rounded-full outline-none ring-[3px] ring-background focus-visible:ring-ring"
          style={{ width: size, height: size, zIndex: hovered === i ? 20 : shown.length - i }}
        >
          {p.src ? (
            <img src={p.src} alt="" width={size} height={size} className="size-full rounded-full object-cover" />
          ) : (
            <span
              className="grid size-full place-items-center rounded-full bg-secondary font-medium text-secondary-foreground"
              style={{ fontSize: size * 0.36 }}
            >
              {initials(p.name)}
            </span>
          )}
          <AnimatePresence>
            {hovered === i && (
              <motion.span
                aria-hidden
                initial={{ opacity: 0, y: 6, x: "-50%", scale: 0.85 }}
                animate={{ opacity: 1, y: 0, x: "-50%", scale: 1 }}
                exit={{ opacity: 0, y: 4, x: "-50%", scale: 0.9, transition: { duration: 0.1 } }}
                transition={{ type: "spring", stiffness: 600, damping: 26 }}
                className="pointer-events-none absolute bottom-[calc(100%+10px)] left-1/2 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background"
              >
                {p.name}
              </motion.span>
            )}
          </AnimatePresence>
        </motion.div>
      ))}
      {extra > 0 && (
        <motion.span
          animate={{ marginLeft: spread ? 4 : -overlap }}
          transition={spring}
          className="relative grid shrink-0 place-items-center rounded-full bg-muted font-mono text-xs text-muted-foreground ring-[3px] ring-background"
          style={{ width: size, height: size }}
        >
          +{extra}
        </motion.span>
      )}
    </div>
  );
}
```