Skip to content
Lenso UI

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

PropTypeDefaultDescription
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
maxnumber-Maximum number of avatar children to render; omit to show all
isGridbooleanfalseWhether 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
classNamestring-Additional CSS classes
childrenReact.ReactNode-Avatar components (and optional AvatarGroup.Count) to group together

AvatarGroup.Count

Composes Avatar + Avatar.Fallback for the overflow indicator. Not truncated by max.

PropTypeDefaultDescription
childrenReact.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
classNamestring-Additional CSS classes

Note

  • AvatarGroup uses React Context to pass size, color, and variant to direct Avatar children only
  • max truncates 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 by max and suppresses the auto count. Prefer Count over the removed v2 total prop
  • isGrid disables overlap and uses a wrapping grid layout instead
  • overlap defaults to "clip" (crescent seam); use "ring" for the solid outline; no effect with isGrid
  • No default role="group" — when the stack is meaningful (assignees, viewers), pass role="group" with aria-label or aria-labelledby