Display

Avatar Group

An overlapping avatar stack with ring cutouts, letter fallbacks for broken images, and a computed +N overflow chip.

Preview in your theme

Loading preview…

Registry dependencies
utils
"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

const SIZES = {
  sm: { avatar: "size-6 text-[10px]", overlap: "-space-x-1.5" },
  md: { avatar: "size-8 text-xs", overlap: "-space-x-2" },
  lg: { avatar: "size-10 text-sm", overlap: "-space-x-2.5" },
} as const

/** Shared shell for a disc: ring-background matches the page, so the overlap reads as a cut-out */
const CIRCLE =
  "relative flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/avatar-group.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "AvatarGroup" component (no runtime deps
beyond the cn() class-merge helper).

Contract
- export interface AvatarGroupItem { src?: string; alt: string; fallback?: string }
  — alt is required and always reaches the rendered <img>; fallback defaults to
  the first letter of alt, uppercased.
- export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement>
  { items: AvatarGroupItem[]; max?: number; size?: "sm" | "md" | "lg" }.
- Defaults: max = 5, size = "md". forwardRef to the root div, merge className
  via cn(), spread the remaining props on the root.
- Render the first max items; when items.length > max, append a "+N" chip with
  N = items.length - max — always computed from the data, never hardcoded.

Behavior
- Overlap: the root is a flex row with a per-size negative gap (-space-x-1.5 /
  -space-x-2 / -space-x-2.5 for sm / md / lg); later siblings naturally paint
  on top of earlier ones.
- Hover reveal: hovering one avatar raises it above its neighbors (hover:z-10)
  and lifts it slightly (hover:-translate-y-1, transition-transform ~200ms).
  Under prefers-reduced-motion the transition and lift are disabled
  (motion-reduce:transition-none motion-reduce:hover:translate-y-0); the z
  raise stays because it is a functional reveal, not decoration.
- Image fallback: each avatar is a tiny stateful subcomponent that remembers
  WHICH src failed (not a boolean), so a changed src gets a fresh attempt.
  When src is missing, the img fires onError, or it had already failed before
  hydration (a ref callback finds it complete with naturalWidth 0 and
  confirms with img.decode(), which rejects only for a broken image — a
  viewBox-only SVG can also report 0), swap the <img> for a letter layer
  showing fallback ?? alt[0]. A broken-image glyph never shows.
- Accessibility: the <img> carries the real alt; the letter layer carries
  role="img" aria-label={alt}, so every avatar keeps its accessible name either
  way. The "+N" chip appends a visually-hidden " more" so screen readers hear
  "+2 more". Nothing is focusable — the stack is purely presentational.
- No built-in tooltip and no click handlers; consumers compose those around
  the component (see levers).

Rendering & styling
- Semantic tokens only. Every circle wears ring-2 ring-background — the ring
  matches the page background and carves the cutout seam between overlapping
  avatars, in light and dark mode alike. Letter fallbacks and the "+N" chip
  use bg-muted text-muted-foreground. No hex values, no palette classes.
- Circles: rounded-full overflow-hidden shrink-0 select-none; the img is
  size-full object-cover. Sizes sm / md / lg map to size-6 / size-8 / size-10
  with text sizes text-[10px] / text-xs / text-sm.
- Keep the size → { avatar classes, overlap class } mapping in one const
  object so adding a size is a one-line change.

Customization levers
- Overlap density: the per-size -space-x-* values are the single overlap knob —
  -space-x-3 on lg reads as a dense facepile, -space-x-1 as a loose row.
- Stacking direction: by default later avatars paint on top; to put the first
  member on top instead, add flex-row-reverse -space-x-reverse justify-end to
  the root className and reverse the items array — visual order is preserved,
  paint order inverts.
- Tooltip with names on hover: wrap the stack in shadcn <TooltipProvider> and,
  inside the item loop, wrap each avatar in <Tooltip><TooltipTrigger asChild>
  …</TooltipTrigger><TooltipContent>{item.alt}</TooltipContent></Tooltip> —
  the alt string already carries the display name.
- Click-to-expand roster: turn the "+N" chip into a <button> that opens a
  shadcn Popover or DropdownMenu listing items.slice(max) with full names;
  keep the individual avatars non-interactive.
- Extra size: add one entry to the size map (e.g. xl → size-12 text-base with
  -space-x-3); nothing else changes.
- Ring width: ring-2 is tuned for size-6…10 circles; bump toward ring-[3px]
  for size-12 and up so the seam keeps its proportion.

Concepts

  • Ring cutout — each circle wears a ring in the page-background color, so the overlap seam looks carved without any mask tricks, and it recolors itself in dark mode for free.
  • Computed overflow — the "+N" chip is derived from items.length - max at render time; the count can never drift from the data it summarizes.
  • Hover reveal — a partially hidden avatar becomes fully visible through a z-index raise; the slight lift is decorative and turns off under prefers-reduced-motion, while the reveal itself stays functional.
  • Graceful image degradation — a missing or failing src swaps the image for an initials layer; the accessible name (alt) survives the swap and a broken-image glyph never renders.
  • Glance, not roster — a facepile answers "who is on this?" in one glance; managing members (names, roles, actions) belongs to a list or table, not to this stack.

On This Page