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
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/avatar-group.jsonPrompt
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 - maxat 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
srcswaps 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.