Skip to content
goodtabs UI
BrowseBrowse components

Avatar group

Overlapping avatars with initials fallback and a +n count for the rest.

npx shadcn@latest add https://ui.goodtabs.co/r/avatar-group.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/avatar-group

Seven people, four shown, then +3.

Sizes

Small, default and large. A short list shows no count.

Usage

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

<AvatarGroup people={[{ name: "Priya Nair" }, { name: "Hana Sato" }]} max={4} />

Props

PropTypeDefaultDescription
people{ name: string; src?: string }[]Everyone in the group. Initials show without an image.
maxnumber4Avatars shown before the +n count.
size"sm" | "default" | "lg""default"24px, 32px or 40px.

Source

avatar-group/avatar-group.tsx
import * as React from "react";
import {
  Avatar,
  AvatarFallback,
  AvatarGroup as AvatarStack,
  AvatarGroupCount,
  AvatarImage,
} from "@/components/ui/avatar";
import { cn } from "@/lib/utils";

export type AvatarGroupPerson = {
  name: string;
  /** Image URL. Initials show when it is missing or fails. */
  src?: string;
};

type AvatarGroupProps = Omit<React.ComponentProps<"div">, "children"> & {
  people: AvatarGroupPerson[];
  /** How many avatars to show before the +n count. */
  max?: number;
  /** Avatar size: 24px, 32px or 40px. */
  size?: "sm" | "default" | "lg";
};

function initials(name: string) {
  return name
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => part[0]?.toUpperCase())
    .join("");
}

/**
 * Overlapping avatars with a +n count for the rest. A 2px ring in the background colour cuts each avatar from the next.
 * The group has one accessible name listing everyone, so the visible faces are decoration.
 */
export function AvatarGroup({ people, max = 4, size = "default", className, ...props }: AvatarGroupProps) {
  const shown = people.slice(0, max);
  const extra = people.length - shown.length;
  const names = people.map((p) => p.name);
  const label =
    names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : (names[0] ?? "No one");
  const text = size === "lg" ? "text-sm" : "text-xs";

  return (
    <AvatarStack role="img" aria-label={label} data-slot="avatar-group-root" className={cn(className)} {...props}>
      {shown.map((person) => (
        <Avatar key={person.name} size={size} aria-hidden="true">
          {person.src && <AvatarImage src={person.src} alt="" />}
          <AvatarFallback className={cn("text-foreground", text)}>{initials(person.name)}</AvatarFallback>
        </Avatar>
      ))}
      {extra > 0 && (
        <AvatarGroupCount aria-hidden="true" className={cn("font-mono text-foreground tabular-nums", "text-xs")}>
          +{extra}
        </AvatarGroupCount>
      )}
    </AvatarStack>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/avatar-group/prompt.md, or the Copy AI prompt button above.

When to use

  • Showing who is on a project, a thread or a document in a small space
  • Card footers and table cells that list a few people

When not to use

  • When people need to be clicked or hovered one by one: use a list
  • A single person: use Avatar

Agent notes

  • Built on the avatar primitives; the group is one role img whose label lists every name
  • Initials come from the first two words of the name
  • The count is the number of people past max, so it is hidden when everyone fits