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.jsonUsage
import { AvatarStack, AvatarStackItem, AvatarStackOverflow } from "@/components/avatar-stack"Paths assume the default @/ alias from your components.json. The example above shows the parts composed.
API
<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 | none |
alt | string | none |
fallback | React.ReactNode | none |
asChildWhen true, renders as the child element (e.g. an anchor or button) via Radix Slot. | boolean | false |
children | React.ReactNode | none |
<AvatarStackOverflow />
+ native element props| Prop | Type | Default |
|---|---|---|
count* | number | none |
prefix | string | '+' |
asChild | boolean | false |
children | React.ReactNode | none |
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>;
const AvatarStack = ({ className, size = 'md', spacing = 'normal', ...props }: AvatarStackProps) => {
return (
<div
data-slot="avatar-stack"
data-size={size}
className={cn(avatarStackVariants({ size, spacing }), className)}
{...props}
/>
);
};
interface AvatarStackItemProps extends 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;
}
const 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>
);
};
interface AvatarStackOverflowProps extends Omit<React.ComponentProps<'span'>, 'children'> {
count: number;
prefix?: string;
asChild?: boolean;
children?: React.ReactNode;
}
const 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