Avatar Stack
Overlapping avatar group with size (sm/md/lg) and spacing (tight/normal/loose) variants, image or fallback support, numeric overflow chip, and asChild on items/overflow so each avatar can render as a link or button.
Example
Installation
npx shadcn@latest add https://hirael.com/r/avatar-stack.jsonAPI
<AvatarStack />
+ native element props| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | null | "md" |
spacing | "tight" | "normal" | "loose" | null | "normal" |
<AvatarStackItem />
+ native element props| Prop | Type | Default |
|---|---|---|
src | string | — |
alt | string | — |
fallback | React.ReactNode | — |
asChildWhen true, renders as the child element (e.g. an anchor or button) via Radix Slot. | boolean | false |
children | React.ReactNode | — |
<AvatarStackOverflow />
+ native element props| Prop | Type | Default |
|---|---|---|
prefix | string | "+" |
count* | number | — |
asChild | boolean | false |
children | React.ReactNode | — |
Component source
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
const avatarStackVariants = cva("flex items-center", {
variants: {
size: {
sm: "[&>[data-slot='avatar-stack-item']]:size-6 [&>[data-slot='avatar-stack-item']]:text-[10px]",
md: "[&>[data-slot='avatar-stack-item']]:size-8 [&>[data-slot='avatar-stack-item']]:text-xs",
lg: "[&>[data-slot='avatar-stack-item']]:size-10 [&>[data-slot='avatar-stack-item']]:text-sm",
},
spacing: {
tight: "[&>[data-slot='avatar-stack-item']:not(:first-child)]:-ms-3",
normal: "[&>[data-slot='avatar-stack-item']:not(:first-child)]:-ms-2",
loose: "[&>[data-slot='avatar-stack-item']:not(:first-child)]:-ms-1",
},
},
defaultVariants: {
size: "md",
spacing: "normal",
},
});
type AvatarStackProps = React.ComponentProps<"div"> &
VariantProps<typeof avatarStackVariants>;
function AvatarStack({
className,
size = "md",
spacing = "normal",
...props
}: AvatarStackProps) {
return (
<div
data-slot="avatar-stack"
data-size={size}
className={cn(avatarStackVariants({ size, spacing }), className)}
{...props}
/>
);
}
type AvatarStackItemProps = Omit<React.ComponentProps<"span">, "children"> & {
src?: string;
alt?: string;
fallback?: React.ReactNode;
/** When true, renders as the child element (e.g. an anchor or button) via Radix Slot. */
asChild?: boolean;
children?: React.ReactNode;
};
function AvatarStackItem({
className,
src,
alt,
fallback,
asChild = false,
children,
...props
}: AvatarStackItemProps) {
const Comp = asChild ? Slot : "span";
const content = src ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt={alt ?? ""}
loading="lazy"
className="absolute inset-0 size-full object-cover"
/>
) : (
(fallback ?? children)
);
return (
<Comp
data-slot="avatar-stack-item"
className={cn(
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-mono font-medium text-foreground ring-2 ring-background",
asChild &&
"transition-transform duration-150 ease-out hover:z-10 hover:scale-105 focus-visible:z-10 focus-visible:outline-none focus-visible:ring-ring",
className,
)}
{...props}
>
{asChild ? children : content}
</Comp>
);
}
type AvatarStackOverflowProps = Omit<
React.ComponentProps<"span">,
"children"
> & {
count: number;
prefix?: string;
asChild?: boolean;
children?: React.ReactNode;
};
function AvatarStackOverflow({
className,
count,
prefix = "+",
asChild = false,
children,
...props
}: AvatarStackOverflowProps) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="avatar-stack-item"
data-overflow=""
className={cn(
"relative inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-card font-mono font-medium tabular-nums text-muted-foreground ring-2 ring-background",
asChild &&
"transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
className,
)}
{...props}
>
{asChild ? (
children
) : (
<>
{prefix}
{count}
</>
)}
</Comp>
);
}
export { AvatarStack, AvatarStackItem, AvatarStackOverflow };
Dependencies
npm
@radix-ui/react-slotclass-variance-authority