AvatarGroup
Shows who is involved without spending a full row per person. Use it for project members,
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
AvatarGroup is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { AvatarGroup } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Shows who is involved without spending a full row per person. Use it for project members, shared documents, and assignee columns.
Rules
labelis required. Screen readers announce the group, not each overlapping image.- Keep
maxat 3–5. Past that the overlap stops being readable and the +N chip carries no useful information. - Order matters: put the most relevant people first. Do not sort alphabetically unless the list is a directory.
- Do not make individual avatars in a group clickable. If a person needs an action, use a List instead.
Tradeoffs
Overlap saves horizontal space but hides parts of each face. In a directory or a picker where identification matters more than density, use a List with full avatars.
Props
The typed contract, from AvatarGroup.d.ts.
import * as React from "react";
import { AvatarProps } from "./Avatar";
/** Overlapping row of avatars with a count for the remainder. */
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLSpanElement> {
/** People to show, in display order. */
people: Array<Pick<AvatarProps, "name" | "src">>;
/** How many avatars render before the +N chip. @default 4 */
max?: number;
/** @default "md" */
size?: "xs" | "sm" | "md" | "lg" | "xl";
/** Accessible group label, e.g. "Project members". Required. */
label: string;
}
export declare function AvatarGroup(props: AvatarGroupProps): React.JSX.Element;
Tokens it reads
Every custom property AvatarGroup.jsx resolves, read from the source: 8 semantic, 1 primitive. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
- primitive Raw values. A component reading one directly is a deliberate exception.
| Token | Tier | Declared as |
|---|---|---|
--dt-border-subtle | semantic | var(--dt-color-neutral-100) |
--dt-border-width-default | semantic | var(--dt-dim-hair) |
--dt-radius-pill | semantic | var(--dt-radius-raw-full) |
--dt-surface-base | semantic | var(--dt-color-white) |
--dt-surface-sunken | semantic | var(--dt-color-neutral-100) |
--dt-text-label-sm-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-sm-size | semantic | var(--dt-font-size-xs) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-font-weight-medium | primitive | 500 |
Source
React only, no dependencies, every value a token.
import React from "react";
import { Avatar } from "./Avatar.jsx";
const SIZES = { xs: 20, sm: 24, md: 32, lg: 40, xl: 56 };
export function AvatarGroup({ people = [], max = 4, size = "md", label, style, ...rest }) {
const shown = people.slice(0, max);
const overflow = people.length - shown.length;
const px = SIZES[size] || SIZES.md;
return (
<span role="group" aria-label={label} style={{ display: "inline-flex", alignItems: "center", ...style }} {...rest}>
{shown.map((p, i) => (
<span key={p.name + i} style={{ marginLeft: i === 0 ? 0 : -(px * 0.3), borderRadius: "var(--dt-radius-pill)", boxShadow: "0 0 0 2px var(--dt-surface-base)", display: "inline-flex" }}>
<Avatar {...p} size={size} />
</span>
))}
{overflow > 0 && (
<span style={{
marginLeft: -(px * 0.3), width: px, height: px, borderRadius: "var(--dt-radius-pill)",
background: "var(--dt-surface-sunken)", color: "var(--dt-text-secondary)",
display: "inline-flex", alignItems: "center", justifyContent: "center",
fontFamily: "var(--dt-text-label-sm-family)", fontSize: "var(--dt-text-label-sm-size)",
fontWeight: "var(--dt-font-weight-medium)", boxShadow: "0 0 0 2px var(--dt-surface-base)",
border: "var(--dt-border-width-default) solid var(--dt-border-subtle)",
}}>+{overflow}</span>
)}
</span>
);
}
In context
AvatarGroup also appears on the display reference cards, alongside the rest of its family.