Bridge 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).
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 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.
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 |
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps stack / grid layout variant. |
Size | Enum | size | Maps xs / sm / md / lg / xl; mirrors Avatar size. |
Overflow Direction | Enum | overflowDirection | end (default, last DOM avatar on top) or start (first DOM avatar on top). |
Interactive | Boolean | interactive | When true, overflow indicator renders as `<button>` and opens a popover; when false, indicator is a labelled `<span>`. |
Max | Number | max | Bound on visible avatar count; counts beyond render as +N indicator. |
Total | Number | total | Optional explicit total; when present, overrides children length for surplus computation. |
Avatars | Slot | avatar | Repeatable slot for child Avatar instances. |
Overflow Popover | Slot | overflow-popover | Optional disclosure surface; required when interactive is true. |
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) |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
container | frame | Auto-layout frame; horizontal direction for stack variant, wrap-grid for grid variant; negative item-spacing for stack-overlap effect |
avatar | instance | Avatar component instance; repeats per max property; z-index increments to layer correctly |
overflow-indicator | frame | Avatar-shaped frame with "+N" centred text; visibility per "has overflow" property |
overflow-popover | frame | Floating panel anchored below the overflow indicator; vertical list of member rows; popover-style elevation |
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 |
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
Internationalisation
RTL · mirroring
Stack overlap direction follows `margin-inline-start` and the `overflowDirection` enum is logical — `end` flips from right-side-on-top (LTR) to left-side-on-top (RTL) automatically. Grid variant tile order also mirrors via writing-direction-aware grid auto-flow. Overflow indicator sits at the inline-end of the stack regardless of language — it follows the direction of growth, not a fixed visual corner.
Text expansion
Overflow indicator text "+5" stays compact across languages. The accessible-name suffix ("more members" → German "weitere Mitglieder" 60 % longer; Russian "еще участников" 80 % longer) expands invisibly to AT but never affects layout. Group `aria-label` ("Document collaborators") follows generic prose expansion — reserve no extra width because the label is not visible.
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.