Dev view

Avatar Group

A composition of avatar instances rendered as a stack or grid with bounded count and an overflow indicator (+N) for collaborator lists, attribution rows, and shared-document headers. Codifies the canonical overflow contract — counts beyond `max` collapse into a single indicator tile — and the disclosure pattern for revealing the full member list.

Also called Avatar stack Stacked avatars User stack Member list

When to use

Use

For representing a group of users, organisations, or entities in a bounded surface — collaborator strips on shared documents, member rows in team headers, attribution lines on commits, and viewer lists in real-time editors. Pair with a textual count or member- list affordance when the overflow indicator is the only access path to the collapsed members. Stack layout suits dense inline contexts (header strips, attribution lines); grid layout suits member-management surfaces (settings pages, organisation rosters).

Avoid

For a single-entity representation — that is `Avatar`, not a group of one. For a long member list with metadata (presence, role, activity timestamp) — that is a list of `ListItem` rows with a leading avatar slot, not a stack. For unbounded counts on dense surfaces — set a sensible `max` (typically 3-5) so the visual weight stays predictable. For decorative groupings without identity meaning — that is layered tiles, not avatars.

Versus related

  • avatar

    `Avatar` is the single-entity representation primitive that Avatar Group composes; Avatar Group is the bounded set representation. A single Avatar is "this person"; an Avatar Group is "this group of people". When the count is one, render Avatar directly — Avatar Group with one child is an anti-pattern.

  • list-item

    `ListItem` is a one-dimensional row with leading avatar + primary text + secondary text + trailing affordances; Avatar Group is a bounded visual unit that collapses identity into a compact strip. ListItem suits member-management with metadata (presence, role, activity); Avatar Group suits inline density where the group is a single visual marker. The decision test: does each member need its own row of metadata (ListItem) or can the group be summarised as a single strip (Avatar Group)?

Avatar Group composes multiple Avatar instances into a single visual unit when a surface needs to communicate "this is a group" rather than "this is a person". Two layouts (stack-overlapped or grid-tiled), a bounded `max` count that triggers a +N overflow indicator, and an optional disclosure that reveals the collapsed members on click. The reference documents the group-role + accessible-name contract, the overflow-tile interactivity rule (button when it opens a popover, static text otherwise), and the divergence between libraries that ship a single AvatarGroup (MUI, Atlassian, HeroUI) and libraries that defer overflow to manual composition (Mantine, Chakra, PrimeReact).

Highlight
Fig 1.1 · Avatar Group · Dev view
Dev

Code anatomy

Slot Code slot Semantic
container container span-with-role-group
avatar avatar avatar-instance
overflow-indicator overflow-indicator button-or-span
overflow-popover overflow-popover dialog-or-listbox
Both

Variants, properties, states

Variants

Structurally different versions of the component.

stack grid

Properties

The same component, parameterised.

PropertyType
size xs | sm | md | lg | xl
overflowDirection end | start
interactive boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visible
data
emptypartialoverflowingexpanded
Both

State transitions

FromToTrigger
overflowingexpandedUser activates overflow indicator (click, Enter, Space)
expandedoverflowingUser dismisses popover (Escape, outside click, indicator re-click)
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-avatar-group>` host with `max`, `total`, `size`, `variant`, `overflow-direction`, `interactive` attributes; renders slotted `<ui-avatar>` children up to max plus an internal overflow tile when total exceeds max attributes (`variant="stack"`, `variant="grid"`, `size="md"`, `interactive`); CSS uses logical properties (`margin-inline-start`) for overlap direction
React Function component (Material UI AvatarGroup, Atlassian AvatarGroup, HeroUI AvatarGroup, PrimeReact AvatarGroup) wrapping a span / div; children-based composition where the parent computes surplus from children.length and max props with class-variance-authority for variant / size; `max` + `total` + `renderSurplus` props for overflow customisation; `interactive` (or library-specific naming like `onClickMore`) toggles button-vs-span overflow tile
Angular (signals) Angular component with input<number>('max'), input<number | undefined>('total'), input<'stack' | 'grid'>('variant'); content-projects child Avatar instances via ng-content with selector input<'xs' | 'sm' | 'md' | 'lg' | 'xl'>(); host-binding [attr.role]="'group'" plus [attr.aria-label]; signal-derived overflow count from contentChildren
Vue Single-file component with default slot for avatar children; PrimeVue AvatarGroup, Naive UI AvatarGroup as third-party precedents; computed property derives surplus from slot children length and max defineProps with literal-union types; v-if for overflow indicator rendering
Both

Events

  1. overflowActivateoptional
    Payload
    `{ surplus: number }`. Fires when the user activates the overflow indicator (click, Enter, Space). Only canonical when the group is interactive (opens a popover); static groups never observe it.
    Web Components
    `overflowActivate` CustomEvent on the host with `event.detail = { surplus }`.
    React
    `onOverflowActivate(surplus)` callback; or a generic `onClick` on the overflow indicator with library-internal surplus computation.
    Angular Signals
    `output<{ surplus: number }>('overflowActivate')`.
    Vue
    `@overflow-activate` event with payload `{ surplus }`.
  2. expandedChangeoptional
    Payload
    `{ expanded: boolean }`. Fires when the popover open-state changes (open or dismiss). Only canonical when the consumer controls the popover state externally; uncontrolled groups manage state internally.
    Web Components
    `expandedChange` CustomEvent with `event.detail = { expanded }`.
    React
    `onExpandedChange(expanded)` callback (matches the controlled-disclosure idiom).
    Angular Signals
    `output<boolean>('expandedChange')`.
    Vue
    `@expanded-change` event or `v-model:expanded` two-way binding.
Both

Accessibility

Slot Accessibility hint
container Carries `role="group"` plus `aria-label` describing the group ("Document collaborators (5)", "Team A members"). Without the label, SR users hear a sequence of individual avatar names without group context. The accessible name should include the total member count when the overflow indicator does not announce it.
avatar Each child Avatar carries its own accessible name (per the Avatar canon — image variant via `<img alt>`, initials / icon variants via `role="img"` + `aria-label`). The container's `role="group"` collects them; SR announces "group, Alex Black, Jamie Lee, Sam Reyes, +4 more".
overflow-indicator When interactive, `<button>` with `aria-label="N more members"` (or "+N more") plus `aria-expanded` reflecting the popover state. When static, `<span>` with the visible "+N" text and a visually-hidden "more members" suffix. Never rely on the literal "+N" alone — SR users without context cannot decode the plus sign.
overflow-popover The popover follows the disclosure pattern (APG): the overflow-indicator carries `aria-expanded` plus `aria-controls` referencing the popover; focus moves into the popover on open; Escape closes and returns focus to the indicator. When the popover is a list of selectable members, use `role="listbox"` with arrow-key navigation; when it is a read-only overview, use a generic container with a heading.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
Tab (static group, no overflow popover)Static avatar groups are not in the tab order — the group is decorative-or-informational and individual avatars are not focusable. Tab moves through the surrounding host controls (sidebar, header, list).
Tab (interactive group, overflow indicator opens popover)Tab lands on the overflow indicator (a `<button>`) as a single tab stop. The avatar slots themselves remain non- focusable unless individually wrapped in their own buttons / links by the host context (member-link composition).
Enter, Space (overflow indicator focused)Activates the indicator. Popover opens; focus moves into the popover (first focusable element or the popover container with `tabindex="-1"`). `aria-expanded` flips to true.
Escape (popover open)Closes the popover. Focus returns to the overflow indicator. `aria-expanded` flips back to false.
Tab (popover open, focus inside popover)Moves through focusable elements inside the popover (member rows, dismiss button). When the popover is a `role="listbox"` member-list, arrow keys navigate instead of Tab.

Screen-reader announcements

TriggerExpected
SR encounters the avatar group containerSR announces the group role and accessible name first ("group, Document collaborators, 8 members"). Reading continues with the visible avatars in DOM order ("Alex Black, Jamie Lee, Sam Reyes") and ends with the overflow indicator ("button, 5 more members, collapsed").
User activates the overflow indicatorSR announces the popover open ("dialog" or "list, 5 items") and reads the first focusable element. The indicator's `aria-expanded` flip is announced.
Popover dismissesSR announces focus return to the indicator. The `aria-expanded="false"` state is reflected in the next announcement.

axe-core rules to assert

  • aria-allowed-attr
  • aria-required-attr
  • aria-valid-attr-value
  • button-name
  • color-contrast

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/avatar-group/a11y-fixture.json

Both

Contracts

Non-negotiable contracts

  1. APGWAI-ARIA role=group plus accessible-name pattern

    The container carries `role="group"` plus `aria-label` describing the group. Without either, SR users hear a sequence of individual avatar names without the "this is a group" semantic. The label should include the total member count when the overflow indicator does not surface it independently.

    Without the role and label, the group is a sighted-only composition. AT users encounter ambiguous avatar sequences — the membership boundary, the count, and the group's purpose all collapse into a flat list of names.

  2. Canon

    Counts beyond `max` collapse into a single overflow indicator with the literal surplus ("+N"). The indicator is non-optional whenever total count > `max`. Implementations that hide surplus avatars without the indicator communicate a smaller membership than reality.

    Without the indicator, users perceive the group as smaller than it is. The collapsed members lose representation; the membership count becomes lossy.

  3. APGAPG: Disclosure pattern

    The overflow indicator is `<button>` whenever it opens a popover (interactive group). It carries `aria-expanded` reflecting the popover state and `aria-controls` referencing the popover. Static groups (no popover) render the indicator as a labelled `<span>`. Implementations that ship a `<div>` with onClick lose keyboard activation, focus order, and disclosure semantics.

    Without the button semantic, keyboard users cannot reach the popover. AT users cannot perceive that the indicator is interactive — `aria-expanded` is the canonical signal for "this control toggles disclosed content".

Vocabulary drift

Material 3
Avatar
Material 3 spec defines Avatar but not AvatarGroup as a first-class component; library implementations (Material UI) ship AvatarGroup as a composition utility on top of Avatar. Same canonical contract, different documentation surface.
Atlassian
Avatar Group
Atlassian explicitly supports two layouts (stack or grid) as first-class variants — the rationale for the canonical variant axis. Most other libraries default to stack only.
Polaris
Avatar
Polaris ships Avatar but does not ship a canonical AvatarGroup — composition is left to the consumer. The canon documents this absence as an industry-divergence point, not a gap in the canon itself.
Carbon
User Avatar
Carbon ships User Avatar but no AvatarGroup — same pattern as Polaris. Consumers compose manually with custom layouts.
GOV.UK
GOV.UK Design System does not spec Avatar or AvatarGroup at all — government services rarely surface user-identity strips. The canon documents Avatar Group for the productivity-app context where the pattern is dominant.
Dev

Common mistakes

Blocker

#avatar-group-no-group-role

Container has no `role="group"` or `aria-label`

Problem

The container is a bare `<div>` with no role and no accessible name. SR users hear a sequence of individual avatar names without group context — "Alex Black image, Jamie Lee image, Sam Reyes image" — losing the "this is a collaborator strip" semantic entirely.

Fix

Set `role="group"` on the container plus `aria-label` describing the group ("Document collaborators (5)", "Team A members"). The label should include the total member count when the overflow indicator does not surface it. Avoid `role="list"` — list semantics are for sequential enumeration, not a bounded set.

Blocker

#avatar-group-overflow-no-aria-label

Overflow indicator shows "+N" with no accessible label

Problem

The indicator renders `<span>+5</span>` with no `aria-label`, no visually-hidden suffix, and no associated description. SR users hear "five" or "plus five" with no semantic — the count is ambiguous (more avatars? more members? more icons?).

Fix

When interactive, `<button aria-label="5 more members">+5</ button>` (or "+5 more"). When static, append a visually-hidden suffix — `<span>+5<span class="sr-only"> more members</span> </span>`. Never rely on the literal "+N" — the plus sign is visual shorthand without context for AT.

Major

#avatar-group-overflow-not-button-when-interactive

Overflow indicator is `<div>` or `<span>` but opens a popover

Problem

The indicator opens a member-list popover when clicked but ships as a non-button element (often a div with onClick). The element is not in the keyboard tab order, has no `aria-expanded`, and offers no Enter / Space activation. The popover is mouse-only.

Fix

Render the interactive overflow indicator as `<button>` with `aria-expanded` reflecting the popover state and `aria-controls` referencing the popover id. Static (no popover) overflow uses a `<span>`. The interactive boolean property toggles the rendered semantic — never ship a div that behaves like a button.

Major

#avatar-group-counts-beyond-max-silent-collapse

Counts beyond `max` collapse without the overflow indicator

Problem

The implementation hides avatars beyond `max` but does not render the +N indicator. Users see five collaborators in a ten-collaborator group with no signal that more exist. The group communicates a smaller membership than reality.

Fix

Whenever total count > `max`, render the overflow indicator with the literal surplus ("+N" where N = total - max). The indicator is non-optional in the overflowing state; only the partial state (total ≤ max) omits it.

Major

#avatar-group-popover-no-focus-trap

Popover opens but focus does not move into it

Problem

The disclosure popover opens on indicator click but keyboard focus stays on the indicator. Tab does not enter the popover; Escape does nothing. Keyboard users cannot navigate the member list — the popover is mouse-only despite the indicator being a button.

Fix

On open, move focus into the popover (first focusable element, or the popover container itself with `tabindex="-1"` for read-only lists). Escape closes the popover and returns focus to the indicator. Outside-click closes without focus restoration unless the click landed on another focusable element.

Figma↔Code mismatches
  1. 01
    Figma

    Avatars laid out with positive item-spacing (no overlap)

    Code

    Stack variant uses negative `margin-inline-start` to overlap avatars

    Consequence

    Designers ship the stack variant with non-overlapping avatars (positive gap); developers implement the canonical overlap with negative margins. The two surfaces look like different layouts even though they intend the same. Designers find the overlap "broken"; developers find the spec "underspecified".

    Correct

    Document the stack variant as overlapping by canonical contract (negative inline-start margin per avatar after the first; offset bound to size token). Grid variant uses positive gap and no overlap. Figma component variants enforce both layouts as first-class.

  2. 02
    Figma

    Overflow indicator drawn as static text "+5"

    Code

    Overflow indicator rendered as `<button>` with `aria-label`

    Consequence

    Designers ship "+5" as a text label; developers ship a button with `aria-label="5 more members"`. The visual element looks static but behaves as a focusable, clickable target. Designers do not draw the focus ring or hover state; developers ship them anyway, leading to design / implementation drift on focus affordance.

    Correct

    Document the overflow indicator as `<button>` whenever the group is interactive (opens a popover). Figma carries the button-state variants (default / hover / focused / active) so the focus ring is part of the design. Static overflow (no popover) is the rare case — document it separately as a labelled `<span>`.

  3. 03
    Figma

    Stack overlap direction hard-coded as left-overlaps-right

    Code

    Overlap direction follows `inset-inline-start` (logical, RTL-aware)

    Consequence

    Designers ship a fixed overlap direction (left avatar on top); developers use logical inline properties that flip under RTL. In RTL contexts the LTR Figma file ships with the reverse visual order, but the logical-property code agrees with the RTL reading direction. The two surfaces visually disagree.

    Correct

    Document the overlap direction as logical — `overflowDirection` `end` means the last DOM-order avatar sits on top in LTR and on the bottom in RTL. Figma shows both LTR and RTL frames; code uses logical properties uniformly.

  4. 04
    Figma

    Max count baked into Figma as a fixed-instance count

    Code

    `max` prop drives the runtime bound; total count comes from `total` prop or children length

    Consequence

    Designers ship a 5-avatar variant frame; developers compute visible count from `min(total, max)` and render the indicator from the surplus. The two surfaces decouple — designers may not draw the overflow-indicator state for every avatar count they author.

    Correct

    Document `max` and `total` as canonical properties; Figma illustrates the three data states (partial / overflowing / expanded) for at least one representative count. Code computes visible avatars and surplus from those props at render time.