# Avatar Stack

The AvatarStack component overlaps a row of avatars into a single group, collapses everyone past a limit into a +N chip, and lifts an avatar clear of its neighbours on hover without the row changing width.

- **Category:** Component
- **Docs:** https://uibeats.com/docs/component/avatar-stack
- **Registry:** https://uibeats.com/r/avatar-stack.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

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

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

For social proof beside a headline, or the people on a shared document. It reads as a group up to about eight faces, and worse after that.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `avatars` | `AvatarStackItem[]` | `-` | The people to show, in display order (required) |
| `AvatarStackItem: name` | `string` | `-` | Full name. Used as the image's alt text and as the source of the initials fallback |
| `AvatarStackItem: src` | `string` | `-` | Optional image URL. An entry without one renders its initials instead |
| `max` | `number` | `5` | How many avatars to show before the rest collapse into a +N chip |
| `size` | `number` | `40` | Diameter of one avatar, in pixels |
| `overlap` | `number` | `12` | How far each avatar sits over the one before it, in pixels |
| `ring` | `boolean` | `true` | Draw a ring in the page background colour, to separate the discs |
| `className` | `string` | `''` | Additional CSS classes for styling |

## Example usage

```tsx
"use client";
import { AvatarStack } from "@/components/demo/component/avatar-stack";

const TEAM = [
  { name: "Ava Chen" },
  { name: "Marcus Hale" },
  { name: "Priya Nair" },
  { name: "Diego Ramos" },
  { name: "Yuki Tanaka" },
  { name: "Noor Haddad" },
  { name: "Sam Okafor" },
];

const AvatarStackUsage = () => {
  return (
    <div className="flex items-center gap-3">
      <AvatarStack avatars={TEAM} max={5} />
      <p className="text-sm text-muted-foreground">
        Joined by 2,400 teams this month
      </p>
    </div>
  );
};

export default AvatarStackUsage;
```

## Source

Save as `components/ui/avatar-stack.tsx`.

```tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export interface AvatarStackItem {
  /** Full name: the image's alt text, and what the initials are built from. */
  name: string;
  /** Optional image. An avatar without one falls back to initials. */
  src?: string;
}

interface AvatarStackProps {
  avatars: AvatarStackItem[];
  /** How many avatars to show before the rest collapse into a `+N` chip. */
  max?: number;
  /** Diameter of one avatar, in pixels. */
  size?: number;
  /** How far each avatar sits over the one before it, in pixels. */
  overlap?: number;
  /** Draw a ring in the page background colour, to separate the discs. */
  ring?: boolean;
  className?: string;
}

/** `"Ada Lovelace"` -> `"AL"`. */
function initials(name: string): string {
  const parts = name.trim().split(/\s+/).filter(Boolean);
  if (parts.length === 0) return "?";

  const first = parts[0]?.charAt(0) ?? "";
  const last =
    parts.length > 1 ? (parts[parts.length - 1]?.charAt(0) ?? "") : "";
  return (first + last).toUpperCase();
}

/**
 * A row of overlapping avatars with a `+N` chip for the overflow.
 *
 * Hovering lifts one avatar clear of its neighbours rather than pushing them
 * aside: the row keeps its width, so a "trusted by" line beside it never
 * reflows as the pointer travels along the stack.
 *
 * Every avatar carries its owner's name for a screen reader whether it renders
 * an image or initials: a stack of pictures that announces nothing is a row of
 * strangers, and the initials themselves are marked `aria-hidden` because "AC"
 * read aloud is noise.
 */
export function AvatarStack({
  avatars,
  max = 5,
  size = 40,
  overlap = 12,
  ring = true,
  className = "",
}: AvatarStackProps) {
  const prefersReducedMotion = useReducedMotion();

  const visible = avatars.slice(0, Math.max(0, Math.floor(max)));
  const overflow = avatars.length - visible.length;

  const disc = {
    width: size,
    height: size,
    fontSize: Math.max(10, Math.round(size * 0.32)),
  };

  return (
    <div className={cn("flex items-center", className)}>
      {visible.map((avatar, index) => (
        <motion.div
          key={`${avatar.name}-${index}`}
          className={cn(
            "relative flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground select-none hover:z-10",
            ring && "ring-2 ring-background",
          )}
          style={{ ...disc, marginLeft: index === 0 ? 0 : -overlap }}
          whileHover={prefersReducedMotion ? undefined : { y: -4, scale: 1.08 }}
          transition={{ type: "spring", stiffness: 400, damping: 22 }}
        >
          {avatar.src ? (
            /*
             * Stays a plain `img`: a registry component is copied into projects
             * that are not necessarily Next.js, and `next/image` would not
             * resolve there.
             */
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={avatar.src}
              alt={avatar.name}
              className="size-full object-cover"
            />
          ) : (
            <>
              <span aria-hidden="true">{initials(avatar.name)}</span>
              <span className="sr-only">{avatar.name}</span>
            </>
          )}
        </motion.div>
      ))}

      {overflow > 0 && (
        <div
          className={cn(
            "relative flex shrink-0 items-center justify-center rounded-full bg-foreground font-medium text-background select-none",
            ring && "ring-2 ring-background",
          )}
          style={{ ...disc, marginLeft: -overlap }}
        >
          <span aria-hidden="true">+{overflow}</span>
          <span className="sr-only">{overflow} more</span>
        </div>
      )}
    </div>
  );
}

export default AvatarStack;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
