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).
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 |
Variants, properties, states
Variants
Structurally different versions of the component.
stack grid Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | xs | sm | md | lg | xl |
overflowDirection | end | start |
interactive | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visible |
data | emptypartialoverflowingexpanded |
State transitions
| From | To | Trigger |
|---|---|---|
overflowing | expanded | User activates overflow indicator (click, Enter, Space) |
expanded | overflowing | User dismisses popover (Escape, outside click, indicator re-click) |
Cross-framework expression
| Framework | Structure mechanism | Variant 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 |
Events
overflowActivateoptionalexpandedChangeoptional
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. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
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
| Trigger | Expected |
|---|---|
| SR encounters the avatar group container | SR 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 indicator | SR announces the popover open ("dialog" or "list, 5 items") and reads the first focusable element. The indicator's `aria-expanded` flip is announced. |
| Popover dismisses | SR announces focus return to the indicator. The `aria-expanded="false"` state is reflected in the next announcement. |
axe-core rules to assert
aria-allowed-attraria-required-attraria-valid-attr-valuebutton-namecolor-contrast
Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe:
/api/components/avatar-group/a11y-fixture.json
Contracts
Non-negotiable contracts
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.
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.
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.
Common mistakes
#avatar-group-no-group-role
Container has no `role="group"` or `aria-label`
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.
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.
#avatar-group-overflow-no-aria-label
Overflow indicator shows "+N" with no accessible label
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?).
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.
#avatar-group-overflow-not-button-when-interactive
Overflow indicator is `<div>` or `<span>` but opens a popover
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.
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.
#avatar-group-counts-beyond-max-silent-collapse
Counts beyond `max` collapse without the overflow indicator
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.
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.
#avatar-group-popover-no-focus-trap
Popover opens but focus does not move into it
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.
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
- 01 Figma
Avatars laid out with positive item-spacing (no overlap)
CodeStack variant uses negative `margin-inline-start` to overlap avatars
ConsequenceDesigners 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".
CorrectDocument 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.
- 02 Figma
Overflow indicator drawn as static text "+5"
CodeOverflow indicator rendered as `<button>` with `aria-label`
ConsequenceDesigners 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.
CorrectDocument 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>`.
- 03 Figma
Stack overlap direction hard-coded as left-overlaps-right
CodeOverlap direction follows `inset-inline-start` (logical, RTL-aware)
ConsequenceDesigners 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.
CorrectDocument 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.
- 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
ConsequenceDesigners 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.
CorrectDocument `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.