Skip to content

AvatarGroup

MD · 1950B gzip budget

An overlapping avatar stack with an accessible overflow indicator.

Category
Data display
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/avatar-group.json

Copies the source into your project. Pulls in 2 shared registry items: cn, avatar.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

import { cn } from "../lib/cn";
import { Avatar } from "./avatar";

export interface AvatarGroupProps {
  people: Array<{ name: string; src?: string }>;
  max?: number;
  size?: number;
  className?: string;
}

/**
 * An overlapping stack of avatars with a "+N" overflow indicator.
 *
 * The overflow indicator gets a real accessible name listing who it hides —
 * "+3 more: Aisha, Bo, Chidi" — rather than leaving a screen reader user with
 * an unexplained number.
 */
export function AvatarGroup({ people, max = 4, size = 32, className }: AvatarGroupProps) {
  const visible = people.slice(0, max);
  const overflow = people.slice(max);

  return (
    <ul
      className={cn("flex -space-x-2 rtl:space-x-reverse", className)}
      aria-label={`${people.length} people`}
    >
      {visible.map((person, index) => (
        <li key={`${person.name}-${index}`} className="ring-2 ring-white rounded-full dark:ring-neutral-950">
          <Avatar name={person.name} src={person.src} size={size} />
        </li>
      ))}

      {overflow.length > 0 && (
        <li className="ring-2 ring-white rounded-full dark:ring-neutral-950">
          {/* role="img" belongs on the span, not the <li> — axe flags role="img"
              as disallowed directly on a list item, and it also strips the
              item's implicit listitem role from the accessibility tree. */}
          <span
            role="img"
            aria-label={`+${overflow.length} more: ${overflow.map((p) => p.name).join(", ")}`}
            className="flex items-center justify-center rounded-full bg-neutral-300 font-medium text-neutral-700 dark:bg-neutral-600 dark:text-neutral-100"
            style={{ width: size, height: size, fontSize: size * 0.35 }}
          >
            <span aria-hidden="true">+{overflow.length}</span>
          </span>
        </li>
      )}
    </ul>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation