flyingsalmon

Avatar Group

A compact roster of people: trip members, collaborators, assignees on a row. Presentational — nothing in the group triggers an action.

Preview

Five people with the default max of 4: four overlapped avatars and a +1 chip. The first avatar sits on top and the z-order descends to the right, so the cluster reads left to right. Every avatar and the chip wear a 2px ring in --background that the group owns — a standalone avatar stays ring-less.

Light
+1
Dark
+1

On a card in dark mode the ring should match the surface it sits on. Override it at the call site with className="[&>*]:ring-card"; there is no prop for it.

Sizes

The two avatar sizes, default and sm, set once on the group. Overlap is a quarter of the diameter: 8px at default, 6px at sm. Use sm in dense rows such as a table cell or a comment header.

Light
+1
+1
Dark
+1
+1

Overflow and max

max is the number of avatars shown, not the total. Whenever the roster runs past it the group renders a chip reading +N, where N is everyone left over — one over max still gets a chip, with no special case. Hidden people are not rendered at all, so a 250-person roster costs five nodes. The chip is an avatar-shaped neutral on --secondary, not a ninth avatar hue and not a badge.

Light
+3
+1
Dark
+3
+1

cap

A roster of 254 with max 4 leaves 250 hidden, and +250 is wider than the circle it sits in. cap caps the printed number at +99 while the chip's accessible name still states the true count, so a screen reader hears “250 more”. Below the cap the chip prints the real number.

Light
+99
+1
Dark
+99
+1

Hover and keyboard

Hover an avatar and the row parts around it over --motion-base. Everything to its left slides 14px left and everything to its right slides 14px right — 12px at sm — which clears the overlap, both 2px rings, and 2px of air. One step, not a fan: the faces beyond the two neighbours travel with them and stay overlapped with each other, so only the hovered avatar comes free. It stands whole without being lifted over anyone, the resting z-order never moves, and it holds its own place so the pointer cannot lose it. A tooltip opens with the person's name. The chip does the same and lists everyone it hides, comma-joined.

Sweeping across the row re-centers the parting on the face under the pointer; the gaps it opens hold the current face rather than closing the row mid-sweep. The row shuts when the pointer leaves the group. It parts 14px past the group's own box on each side, so keep it clear of a clipped container.

Pressing an avatar does nothing — nothing here is a button — but it closes the tooltip, because the floating layer closes every tooltip on any pointer-down. The name comes back when the pointer leaves the avatar and returns.

The group is one tab stop. Tab enters on the first avatar; ArrowRight and ArrowLeft walk the avatars and then the chip; Home and End jump to the ends. Nothing wraps, and Tab leaves the group. Focus does everything hover does and additionally recolors the separator ring to --primary, which keeps the indicator visible on a one-person group where the parting has no neighbour to move. Escape closes the open tooltip and leaves focus where it is.

+1

There is no touch path — the tooltip inherits that limitation, so names and the chip's list are unreachable on a phone. When the names are essential, render them beside the group as text and let the cluster stay decorative.

+2

Ada, Grace and Katherine and 2 others

Provider requirement

The group consumes the registry Tooltip and mounts no provider of its own. Your app root must render TooltipProvider — this docs site mounts one. A local provider per group would reset the shared 300ms skip window, so sweeping the pointer across a row of faces would make you wait out the full open delay on every one.

Accessibility

The wrapper is role="group" and forwards aria-label. Each avatar and the chip is a focusable role="img" named by the person — or alt when there is no name — and N more for the chip. A bare focusable span resolves as generic, a role ARIA forbids naming, so the explicit img role is what makes the name reach assistive tech. An item with neither a name nor an alt is aria-hidden, gets no tooltip, and the arrow keys skip over it. The chip pairs --secondary with --secondary-foreground at roughly 15:1 in both modes, and the initials keep avatar's neutral-950 on step-400 hues at 6.3:1 worst case.

API

items

AvatarGroupItem[]

The roster, in order. Each item picks name, src, color, and alt from Avatar, plus an optional id used as the React key.
size

AvatarSize

default (32px) or sm (24px), set once on the group and pushed to every avatar and the chip. Items carry no size of their own.
max

number

How many avatars render before the overflow chip. Defaults to 4 and clamps up to 1.
cap

number

Caps the number printed in the chip. The chip’s accessible name always states the true count.
aria-label

string

Names the group. Everything else a div takes passes through, except children.