AvatarGroup

Shows who is involved without spending a full row per person. Use it for project members,

Display AvatarGroup.jsx AvatarGroup.d.ts AvatarGroup.md

Playground

Change a prop and watch it. The code below follows along.

Install 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

  • label is required. Screen readers announce the group, not each overlapping image.
  • Keep max at 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.
TokenTierDeclared as
--dt-border-subtlesemanticvar(--dt-color-neutral-100)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-radius-pillsemanticvar(--dt-radius-raw-full)
--dt-surface-basesemanticvar(--dt-color-white)
--dt-surface-sunkensemanticvar(--dt-color-neutral-100)
--dt-text-label-sm-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-font-weight-mediumprimitive500

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.