Skip to content
Primitiv home
Framework
Consumption mode

Avatar Group

stableSource Figma

An overlapping row of Avatars with an optional "+N" overflow counter — the collaborators/attendees pattern. Composes the registry avatar component; the counter is itself an Avatar, not a Badge. Sized xs–xl; data-density scales each size further.

Playground

Density

Preview

ABCDEFGHJK
Size
Direction
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<AvatarGroup size="md" direction="ltr">  <Avatar size="md">    <AvatarImage src="/avatar-1.png" alt="" />    <AvatarFallback>AB</AvatarFallback>  </Avatar>  <Avatar size="md">    <AvatarImage src="/avatar-2.png" alt="" />    <AvatarFallback>CD</AvatarFallback>  </Avatar>  <Avatar size="md">    <AvatarImage src="/avatar-3.png" alt="" />    <AvatarFallback>EF</AvatarFallback>  </Avatar>  <Avatar size="md">    <AvatarImage src="/avatar-4.png" alt="" />    <AvatarFallback>GH</AvatarFallback>  </Avatar>  <Avatar size="md">    <AvatarImage src="/avatar-5.png" alt="" />    <AvatarFallback>JK</AvatarFallback>  </Avatar></AvatarGroup>

Density is set by a data-density ancestor — the Context system, not a AvatarGroup prop.

Installation

npx primitiv add avatar-group

Import

import { AvatarGroup } from "@/components/ui/avatar-group";

No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.

Props

AvatarGroup

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultDescription
childrenReactNode—The Avatar elements.
maxnumber—Show at most this many faces, replacing the remainder with a +N counter. Omit to show every child and no counter.
overflowLabel(count: number) => string(n) => `${n} more`Accessible label for the overflow counter, given the count. The counter is decorative-by-default otherwise.
size"xs" | "sm" | "md" | "lg" | "xl"mdMatches the nested Avatars' size; data-density scales each size further.
direction"ltr" | "rtl"ltrWhich way the stack advances. Decides which end the counter lands on.

Styling contract

--primitiv-avatar-group-overlap--primitiv-avatar-group-ring-width--primitiv-avatar-group-ring-color

Accessibility

  • Name the counter. The +N avatar is decorative by default; pass overflowLabel so it announces something a person can use (&quot;3 more people&quot;, not &quot;+3&quot;). Everything else in the group is your Avatars, whose names you control there.
  • Decide what each face announces. These faces are decorative beside their own initials, so the examples pass alt="" — a screen reader then skips the images rather than reading a filename or a duplicate name. If a face must be named, put the name on that Avatar (an alt, or an aria-label on its Root), not on the group.
  • The group is a plain <div> with no list semantics — it is a visual arrangement, not a list/listitem structure. If the collection is meaningful as a list to assistive technology, wrap it in your own labelled <ul>/<li> around the avatars.
  • size and direction are visual. direction mirrors the layout for RTL but changes nothing about reading order in the accessibility tree; do not use it to convey meaning.
  • Tooltips are not built in. Naming faces on hover would mean the group owning member data, which no Primitiv composite does. Wrap each Avatar in your own Tooltip when you need names — and remember a tooltip needs a focusable trigger, so those avatars must become buttons or links.
  • The --primitiv-avatar-group-ring-color must match the real background for the cutout illusion to hold; a mismatched ring reads as a coloured outline, which is a visual bug rather than an accessibility one but is worth catching in the same pass.

Examples

Overlapping faces (the headline)

A row of Avatars the group overlaps and rings. The faces are your own elements — the group lays them out but cannot set their size, so pass the same size to the group and to each Avatar. Stacking counts into the row: the first face paints on top of the second, and so on. The ring between them is drawn in the surface colour so it reads as a cutout, not a border.

Density
ABCDEFGH
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<AvatarGroup>  <Avatar>    <AvatarImage src="/avatar-1.png" alt="" />    <AvatarFallback>AB</AvatarFallback>  </Avatar>  <Avatar>    <AvatarImage src="/avatar-2.png" alt="" />    <AvatarFallback>CD</AvatarFallback>  </Avatar>  <Avatar>    <AvatarImage src="/avatar-3.png" alt="" />    <AvatarFallback>EF</AvatarFallback>  </Avatar>  <Avatar>    <AvatarImage src="/avatar-4.png" alt="" />    <AvatarFallback>GH</AvatarFallback>  </Avatar></AvatarGroup>

The overflow counter (max)

max shows at most that many faces and folds the rest into a +N counter — the counter is itself an Avatar, not a Badge, so it inherits the ring, size, shape and radius for free (Badge has no neutral tone, and a status colour on an overflow count would be a lie). Give it a real accessible name with overflowLabel, since "+3" alone tells a screen-reader user nothing.

Density
ABCDEFGH+3
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
// members: { id, avatar, initials }[] — 7 people here<AvatarGroup max={4} overflowLabel={(n) => `${n} more people`}>  {members.map((m) => (    <Avatar key={m.id}>      <AvatarImage src={m.avatar} alt="" />      <AvatarFallback>{m.initials}</AvatarFallback>    </Avatar>  ))}</AvatarGroup>

Sizes and density

Five sizes, each rescaling again with the nearest data-density ancestor — the same framed-control/* scale the Avatar itself uses. size sets the overlap and ring width; the reminder bears repeating because it is the one real gotcha here: the group cannot size its children, so size goes on the group and every Avatar inside it.

Density
ABCDEF
ABCDEF
ABCDEF
ABCDEF
ABCDEF
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
{["xs", "sm", "md", "lg", "xl"].map((size) => (  <AvatarGroup key={size} size={size}>    {members.map((m) => (      <Avatar key={m.id} size={size}>        <AvatarImage src={m.avatar} alt="" />        <AvatarFallback>{m.initials}</AvatarFallback>      </Avatar>    ))}  </AvatarGroup>))}

Direction

direction decides which way the stack advances and, with max, which end the counter lands on. "ltr" (the default) advances rightward with the counter trailing on the right; "rtl" mirrors it for a right-to-left locale. Set it to match the reading direction of the surrounding content rather than hand-reordering the children.

Density
ABCDEFGH+3
ABCDEFGH+3
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<AvatarGroup direction="rtl" max={4}>  {members.map((m) => (    <Avatar key={m.id}>      <AvatarImage src={m.avatar} alt="" />      <AvatarFallback>{m.initials}</AvatarFallback>    </Avatar>  ))}</AvatarGroup>

The ring on a tinted background

The separating ring is the surface colour by default, so it reads as a cutout on a default surface. On any other background it will be wrong by construction — set --primitiv-avatar-group-ring-color to that background's colour, on the group itself (the component re-declares its own default, which shadows a value merely inherited from an ancestor).

Density
ABCDEFGH
import { AvatarGroup } from "@/components/ui/avatar-group";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<div style={{ background: "var(--primitiv-surface-sunken)", padding: "1rem" }}>  <AvatarGroup    style={{ "--primitiv-avatar-group-ring-color": "var(--primitiv-surface-sunken)" }}  >    <Avatar>      <AvatarImage src="/avatar-1.png" alt="" />      <AvatarFallback>AB</AvatarFallback>    </Avatar>    <Avatar>      <AvatarImage src="/avatar-2.png" alt="" />      <AvatarFallback>CD</AvatarFallback>    </Avatar>    <Avatar>      <AvatarImage src="/avatar-3.png" alt="" />      <AvatarFallback>EF</AvatarFallback>    </Avatar>    <Avatar>      <AvatarImage src="/avatar-4.png" alt="" />      <AvatarFallback>GH</AvatarFallback>    </Avatar>  </AvatarGroup></div>