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
| Prop | Type | Default | Description |
|---|---|---|---|
| people | { name: string; src?: string }[] | Everyone in the group. Initials show without an image. | |
| max | number | 4 | Avatars 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