deepmoon

Avatar Group

+2
+2
+2

Installation

$ npx shadcn@latest add https://deepmoon.dev/r/avatar-group.json

Props

AvatarGroupProps
PropTypeRequired
avatarsAvatarGroupAvatar[]Yes
maxnumberNo
sizeAvatarGroupSizeNo

Code

avatar-group.tsx
import * as React from "react";

import { cn } from "@/lib/utils";

export type AvatarGroupSize = "sm" | "md" | "lg";

export interface AvatarGroupAvatar {
  src?: string;
  alt: string;
}

export interface AvatarGroupProps extends React.ComponentPropsWithoutRef<"div"> {
  avatars: AvatarGroupAvatar[];
  max?: number;
  size?: AvatarGroupSize;
}

const sizeClass: Record<AvatarGroupSize, string> = {
  sm: "size-6 text-[0.625rem]",
  md: "size-8 text-xs",
  lg: "size-10 text-sm",
};

export function AvatarGroup({ avatars, max = 4, size = "md", className, ...props }: AvatarGroupProps) {
  const visible = avatars.slice(0, max);
  const overflow = avatars.length - visible.length;

  return (
    <div role="group" aria-label={`${avatars.length} people`} className={cn("flex -space-x-2", className)} {...props}>
      {visible.map((avatar, index) => (
        <span
          key={index}
          title={avatar.alt}
          className={cn(
            "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-background bg-muted font-medium text-muted-foreground",
            sizeClass[size],
          )}
        >
          {avatar.src ? (
            <img src={avatar.src} alt={avatar.alt} className="h-full w-full object-cover" />
          ) : (
            <span aria-hidden="true">{avatar.alt.slice(0, 1).toUpperCase()}</span>
          )}
        </span>
      ))}
      {overflow > 0 ? (
        <span
          className={cn(
            "relative inline-flex shrink-0 items-center justify-center rounded-full border-2 border-background bg-secondary font-medium text-secondary-foreground",
            sizeClass[size],
          )}
        >
          +{overflow}
        </span>
      ) : null}
    </div>
  );
}