AvatarGroup
Display a stacked or grid group of avatars with overflow counting
Usage
import { AvatarGroup, Avatar } from '@lenso/ui';"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, AvatarGroup } from "@lenso/ui";import { users } from "./users";
export function Basic() { return ( <AvatarGroup> {users.slice(0, 4).map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((name) => name[0]) .join("")} </Avatar.Fallback> </Avatar> ))} </AvatarGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Anatomy
import { AvatarGroup, Avatar } from '@lenso/ui';
export default () => ( <AvatarGroup> <Avatar> <Avatar.Image /> <Avatar.Fallback /> </Avatar> <AvatarGroup.Count /> {/* Optional explicit child */} </AvatarGroup>);Examples
Max
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, AvatarGroup } from "@lenso/ui";import { users } from "./users";
export function Max() { return ( <AvatarGroup max={3}> {users.map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((n) => n[0]) .join("")} </Avatar.Fallback> </Avatar> ))} </AvatarGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Count
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, AvatarGroup } from "@lenso/ui";import { users } from "./users";
export function Count() { const total = 12; return ( <AvatarGroup size="sm"> {users.slice(0, 3).map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((n) => n[0]) .join("")} </Avatar.Fallback> </Avatar> ))} <AvatarGroup.Count>+{total - 3}</AvatarGroup.Count> </AvatarGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Sizes
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, AvatarGroup } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "../card/display.stylex";import { users } from "./users";
export function Sizes() { const sizes = [ { size: "sm", label: "Small" }, { size: "md", label: "Medium (default)" }, { size: "lg", label: "Large" }, ] as const; return ( <div {...stylex.props(s.centeredColumn6)}> {sizes.map(({ size, label }) => ( <div key={size} {...stylex.props(s.centeredColumn2)}> <p {...stylex.props(s.textSm, s.muted)}>{label}</p> <AvatarGroup size={size}> {users.slice(0, 4).map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((n) => n[0]) .join("")} </Avatar.Fallback> </Avatar> ))} </AvatarGroup> </div> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Grid
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Avatar, AvatarGroup } from "@lenso/ui";import { users } from "./users";
export function Grid() { return ( <AvatarGroup isGrid max={5}> {users.map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((n) => n[0]) .join("")} </Avatar.Fallback> </Avatar> ))} </AvatarGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Overlap
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import type { ReactNode } from "react";import { Avatar, AvatarGroup } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "../card/display.stylex";
const stripes = stylex.keyframes({ to: { backgroundPosition: "24px 24px" } });const styles = stylex.create({ frame: { position: "relative", display: "inline-flex", borderRadius: 12, padding: 24 }, stripes: { pointerEvents: "none", position: "absolute", inset: 0, borderRadius: 12, backgroundColor: "color-mix(in oklab, var(--danger) 6%, var(--surface))", backgroundImage: "repeating-linear-gradient(-45deg, transparent 0 10px, color-mix(in oklab, var(--danger) 12%, transparent) 10px 11px, transparent 11px 21px, color-mix(in oklab, var(--danger) 28%, transparent) 21px 22px)", backgroundSize: "24px 24px", animationName: stripes, animationDuration: "2.8s", animationTimingFunction: "linear", animationIterationCount: "infinite", },});
function StripeBackdrop({ children }: { children: ReactNode }) { return ( <div {...stylex.props(styles.frame)}> <div aria-hidden {...stylex.props(styles.stripes)} /> <div {...stylex.props(s.relativeForeground)}>{children}</div> </div> );}function DemoGroup({ overlap }: { overlap: "clip" | "ring" }) { return ( <AvatarGroup overlap={overlap} size="lg"> <Avatar> <Avatar.Image alt="John" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg" /> <Avatar.Fallback>JD</Avatar.Fallback> </Avatar> <Avatar> <Avatar.Fallback>AB</Avatar.Fallback> </Avatar> <Avatar> <Avatar.Image alt="Emily" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg" /> <Avatar.Fallback>EC</Avatar.Fallback> </Avatar> <Avatar> <Avatar.Fallback>SM</Avatar.Fallback> </Avatar> <AvatarGroup.Count>+2</AvatarGroup.Count> </AvatarGroup> );}export function Overlap() { return ( <div {...stylex.props(s.startColumn6)}> <div {...stylex.props(s.column2)}> <p {...stylex.props(s.textSm, s.muted)}>clip</p> <StripeBackdrop> <DemoGroup overlap="clip" /> </StripeBackdrop> </div> <div {...stylex.props(s.column2)}> <p {...stylex.props(s.textSm, s.muted)}>ring</p> <StripeBackdrop> <DemoGroup overlap="ring" /> </StripeBackdrop> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.// oxlint-disable jsx-a11y/prefer-tag-over-role -- AvatarGroup is a div-based visual group, not a form fieldset.import { Person } from "@gravity-ui/icons";import { Avatar, AvatarGroup } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { s } from "../card/display.stylex";import { users } from "./users";
const styles = stylex.create({ pill: { display: "inline-flex", alignItems: "center", gap: 10, borderRadius: 9999, borderWidth: 1, borderStyle: "solid", borderColor: { default: "color-mix(in oklab, var(--border) 70%, transparent)", ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--border) 80%, transparent)", }, backgroundColor: { default: "color-mix(in oklab, var(--surface) 95%, transparent)", ':is([data-theme="dark"] *)': "color-mix(in oklab, var(--surface) 90%, transparent)", }, paddingBlock: 4, paddingRight: 12, paddingLeft: 4, boxShadow: { default: "0 1px 2px 0 rgb(0 0 0 / 0.05), 0 0 0 1px rgb(0 0 0 / 0.04)", ':is([data-theme="dark"] *)': "0 1px 2px 0 rgb(0 0 0 / 0.05), 0 0 0 1px rgb(255 255 255 / 0.1)", }, }, group: { "--avatar-group-overlap": "0.7rem", "--avatar-group-seam": "2px" },});
export function CustomStyles() { return ( <div {...stylex.props(styles.pill)}> <AvatarGroup aria-label="Assignees" xstyle={styles.group} overlap="clip" role="group" size="sm" > {users.slice(0, 3).map((user) => ( <Avatar key={user.id}> <Avatar.Image alt={user.name} src={user.image} /> <Avatar.Fallback> {user.name .split(" ") .map((part) => part[0]) .join("")} </Avatar.Fallback> </Avatar> ))} <Avatar> <Avatar.Fallback> <Person {...stylex.props(s.icon4, s.shrink0)} /> </Avatar.Fallback> </Avatar> <AvatarGroup.Count>+3</AvatarGroup.Count> </AvatarGroup> <span {...stylex.props(s.textSm, s.medium, s.foreground)}>Assignees</span> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the AvatarGroup component classes, you can use the @layer components directive.
Learn more.
@layer components { .avatar-group { --avatar-group-overlap: 0.75rem; --avatar-group-seam: 2px; }
.avatar-group__count { @apply font-semibold; }}Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The AvatarGroup component uses these CSS classes (View source styles):
Base Classes [!toc]
.avatar-group- Base avatar group container.avatar-group--grid- Grid layout modifier (no overlap).avatar-group--clip- Crescent mask / transparent seam (default stacked overlap).avatar-group--ring- Solid box-shadow outline via--background.avatar-group__count- Overflow count avatar
Stacked layout uses negative margin between siblings (--avatar-group-overlap, default 0.5rem). Clip mode masks overlapped avatars (not the last child / Count) and optically nudges fallback glyphs; ring mode uses a thin box-shadow outline via --background.
API Reference
AvatarGroup
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Size applied to child Avatars (and the overflow count) when they omit size (synced with Avatar) |
color | 'default' | 'accent' | 'success' | 'warning' | 'danger' | - | Color applied to child Avatars (and the overflow count) when they omit color |
variant | 'default' | 'soft' | - | Variant applied to child Avatars (and the overflow count) when they omit variant |
max | number | - | Maximum number of avatar children to render; omit to show all |
isGrid | boolean | false | Whether to use a wrapping grid layout without overlap |
overlap | 'clip' | 'ring' | 'clip' | Stacked overlap style: crescent clip (default) or box-shadow ring; ignored when isGrid |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Avatar components (and optional AvatarGroup.Count) to group together |
AvatarGroup.Count
Composes Avatar + Avatar.Fallback for the overflow indicator. Not truncated by max.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | Count content (e.g. +3) |
size | 'sm' | 'md' | 'lg' | - | Override size from the group |
color | 'default' | 'accent' | 'success' | 'warning' | 'danger' | - | Override color from the group |
variant | 'default' | 'soft' | - | Override variant from the group |
className | string | - | Additional CSS classes |
Note
- AvatarGroup uses React Context to pass
size,color, andvariantto direct Avatar children only maxtruncates visible avatars when set and auto-counts remaining children; omit to show all- For a known total (e.g. from the server), render an explicit
AvatarGroup.Count— it is not truncated bymaxand suppresses the auto count. PreferCountover the removed v2totalprop isGriddisables overlap and uses a wrapping grid layout insteadoverlapdefaults to"clip"(crescent seam); use"ring"for the solid outline; no effect withisGrid- No default
role="group"— when the stack is meaningful (assignees, viewers), passrole="group"witharia-labeloraria-labelledby