Bloom Logo

Avatar Group

Stack multiple avatars together with smooth cubic-bezier hover expansion, orientation support (horizontal or vertical), count truncation, and clean dark/light neutral theme styling.

avatarGroup.tsx

AvatarGroup component for displaying overlapping avatar stacks with butter-smooth hover expansion transitions and orientation control.

ReactTailwindUI ComponentLayoutAvatarGroup
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, type AvatarProps } from "@/components/ui/avatar/avatar";

export type AvatarGroupOrientation = "horizontal" | "vertical";

export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode;
  max?: number;
  total?: number;
  orientation?: AvatarGroupOrientation;
  size?: AvatarProps["size"];
  color?: AvatarProps["color"];
  radius?: AvatarProps["radius"];
  isBordered?: boolean;
  isGrid?: boolean;
  isDisabled?: boolean;
  renderCount?: (count: number) => React.ReactNode;
}

const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
  (
    {
      children,
      max,
      total,
      orientation = "horizontal",
      size = "md",
      color = "default",
      radius = "full",
      isBordered = true,
      isGrid = false,
      isDisabled = false,
      renderCount,
      className,
      ...props
    },
    ref
  ) => {
    const childrenArray = React.Children.toArray(children);
    const countTotal = total ?? childrenArray.length;
    const hasMax = typeof max === "number" && max > 0 && max < childrenArray.length;
    const visibleAvatars = hasMax ? childrenArray.slice(0, max) : childrenArray;
    const excessCount = hasMax ? countTotal - max : countTotal > childrenArray.length ? countTotal - childrenArray.length : 0;

    const isVertical = orientation === "vertical";

    return (
      <div
        ref={ref}
        role="group"
        aria-label="Avatar group"
        className={cn(
          "inline-flex transition-all duration-300 ease-out",
          isGrid
            ? "flex-wrap gap-2"
            : isVertical
            ? "flex-col -space-y-3 hover:-space-y-1.5 items-start"
            : "items-center -space-x-3 hover:-space-x-1.5",
          isDisabled && "opacity-50 grayscale pointer-events-none",
          className
        )}
        {...props}
      >
        {visibleAvatars.map((child, index) => {
          if (!React.isValidElement<AvatarProps>(child)) return child;

          const clonedAvatar = React.cloneElement(child, {
            size: child.props.size || size,
            color: child.props.color || color,
            radius: child.props.radius || radius,
            isBordered: child.props.isBordered !== undefined ? child.props.isBordered : isBordered,
            isDisabled: child.props.isDisabled !== undefined ? child.props.isDisabled : isDisabled,
            className: cn(
              "ring-2 ring-white dark:ring-zinc-900 transition-all duration-300 ease-out",
              child.props.className
            ),
          });

          return (
            <div
              key={index}
              className={cn(
                "relative transition-all duration-300 ease-out hover:z-30 hover:scale-105",
                isVertical ? "hover:translate-x-1" : "hover:-translate-y-1"
              )}
              style={{ zIndex: visibleAvatars.length - index }}
            >
              {clonedAvatar}
            </div>
          );
        })}

        {excessCount > 0 && (
          <div
            className={cn(
              "relative transition-all duration-300 ease-out hover:z-30 hover:scale-105",
              isVertical ? "hover:translate-x-1" : "hover:-translate-y-1"
            )}
            style={{ zIndex: 0 }}
          >
            <Avatar
              size={size}
              color={color}
              radius={radius}
              isBordered={isBordered}
              className="ring-2 ring-white dark:ring-zinc-900"
            >
              {renderCount ? (
                renderCount(excessCount)
              ) : (
                <AvatarFallback className="bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 font-semibold text-xs select-none">
                  +{excessCount}
                </AvatarFallback>
              )}
            </Avatar>
          </div>
        )}
      </div>
    );
  }
);

AvatarGroup.displayName = "AvatarGroup";

export { AvatarGroup };

Default

A standard avatar group component stacking multiple user avatars with smooth hover expansion transitions.

SJ
ED
AR
MK

Orientation (Horizontal & Vertical)

Control the stack direction using orientation (horizontal or vertical). Vertical stacks expand vertically and translate avatars smoothly to the right on hover.

orientation: horizontal | vertical
orientation="horizontal"
H1
H2
H3
orientation="vertical"
V1
V2
V3

Pressable Avatar Stack (isPressable)

Combine AvatarGroup with isPressable to allow users to click individual avatars in an overlapping stack.

SJ
ED
AR

Max Limit & Overflow Badge

Use the max prop to cap visible avatars and automatically display a +N excess count badge formatted with clean dark/light neutral colors.

max: number
A
B
C
+3

Grid Layout (isGrid)

Display avatars in a spaced, non-overlapping grid layout instead of a stack using isGrid.

isGrid: boolean
G1
G2
G3
G4
G5

Sizes

Pass size to the AvatarGroup container to scale all child avatars uniformly.

size: xs | sm | md | lg | xl | 2xl | 3xl
size="sm"
S1
S2
S3
size="md"
M1
M2
M3
size="lg"
L1
L2
L3

Color Themes

Propagate color ring themes to all child avatars via the color prop. Stacked vertically for clear visual comparison.

color: default | primary | secondary | accent | success | warning | danger
color="default"
DF1
DF2
DF3
color="primary"
PR1
PR2
PR3
color="secondary"
SC1
SC2
SC3
color="accent"
AC1
AC2
AC3
color="success"
SU1
SU2
SU3
color="warning"
WR1
WR2
WR3
color="danger"
DG1
DG2
DG3
API Reference

Props — AvatarGroup

Properties for configuring the AvatarGroup component.

PropTypeDefaultDescription
orientation'horizontal' | 'vertical''horizontal'Stack direction scale for grouping avatars.
maxnumberMaximum number of avatars to display before rendering excess (+N) badge.
totalnumberExplicit total user count for calculating overflow badge numbers.
isGridbooleanfalseRenders avatars in a spaced grid layout instead of an overlapping stack.
isDisabledbooleanfalseDisables interaction and applies grayscale filter to all avatars.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl''md'Uniform size scale propagated to all child avatars.
color'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger''default'Color theme applied to outer borders of all avatars in the stack.
radius'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full''full'Border radius scale applied to all avatars in the group.
isBorderedbooleantrueEnables outer ring borders around each avatar in the stack.
renderCount(count: number) => ReactNodeCustom render function for the excess count badge (+N).