Components
Avatar Group
A component that arranges avatars with overlapping visual effects for displaying multiple users or items.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { AvatarGroup } from "@/components/ui/avatar-group";
const avatars = [
{
name: "shadcn",
src: "https://github.com/shadcn.png",
fallback: "CN",
},
{
name: "Ethan Niser",
src: "https://github.com/ethanniser.png",
fallback: "EN",
},
{
name: "Guillermo Rauch",
src: "https://github.com/rauchg.png",
fallback: "GR",
},
{
name: "Lee Robinson",
src: "https://github.com/leerob.png",
fallback: "LR",
},
{
name: "Evil Rabbit",
src: "https://github.com/evilrabbit.png",
fallback: "ER",
},
{
name: "Tim Neutkens",
src: "https://github.com/timneutkens.png",
fallback: "TN",
},
];
export function AvatarGroupDemo() {
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Avatar Group</h3>
<AvatarGroup>
{avatars.slice(0, 4).map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">
Avatar Group with overflow (max 4)
</h3>
<AvatarGroup max={4}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
</div>
);
}Installation
CLI
npx shadcn@latest add "https://diceui.com/r/avatar-group"Manual
Install the following dependencies:
npm install @radix-ui/react-slot class-variance-authorityCopy and paste the following code into your project.
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const avatarGroupVariants = cva("flex items-center", {
variants: {
orientation: {
horizontal: "flex-row",
vertical: "flex-col",
},
dir: {
ltr: "",
rtl: "",
},
},
compoundVariants: [
{
orientation: "horizontal",
dir: "ltr",
className: "-space-x-1",
},
{
orientation: "horizontal",
dir: "rtl",
className: "-space-x-1 flex-row-reverse space-x-reverse",
},
{
orientation: "vertical",
dir: "ltr",
className: "-space-y-1",
},
{
orientation: "vertical",
dir: "rtl",
className: "-space-y-1 flex-col-reverse space-y-reverse",
},
],
defaultVariants: {
orientation: "horizontal",
dir: "ltr",
},
});
interface AvatarGroupProps
extends Omit<React.ComponentProps<"div">, "dir">,
VariantProps<typeof avatarGroupVariants> {
size?: number;
max?: number;
asChild?: boolean;
reverse?: boolean;
}
function AvatarGroup(props: AvatarGroupProps) {
const {
orientation = "horizontal",
dir = "ltr",
size = 40,
max,
asChild,
reverse = false,
className,
children,
...rootProps
} = props;
const childrenArray = React.Children.toArray(children).filter(
React.isValidElement,
);
const itemCount = childrenArray.length;
const shouldTruncate = max && itemCount > max;
const visibleItems = shouldTruncate
? childrenArray.slice(0, max - 1)
: childrenArray;
const overflowCount = shouldTruncate ? itemCount - (max - 1) : 0;
const totalRenderedItems = shouldTruncate ? max : itemCount;
const RootPrimitive = asChild ? Slot : "div";
return (
<RootPrimitive
data-orientation={orientation}
data-slot="avatar-group"
{...rootProps}
className={cn(avatarGroupVariants({ orientation, dir }), className)}
>
{visibleItems.map((child, index) => (
<AvatarGroupItem
key={index}
child={child}
index={index}
itemCount={totalRenderedItems}
orientation={orientation}
dir={dir}
size={size}
reverse={reverse}
/>
))}
{shouldTruncate && (
<AvatarGroupItem
key="overflow"
child={
<div className="flex size-full items-center justify-center rounded-full bg-muted font-medium text-muted-foreground text-xs">
+{overflowCount}
</div>
}
index={visibleItems.length}
itemCount={totalRenderedItems}
orientation={orientation}
dir={dir}
size={size}
reverse={reverse}
/>
)}
</RootPrimitive>
);
}
interface AvatarGroupItemProps
extends Omit<React.ComponentProps<typeof Slot>, "dir">,
VariantProps<typeof avatarGroupVariants> {
child: React.ReactElement;
index: number;
itemCount: number;
size: number;
reverse: boolean;
}
function AvatarGroupItem(props: AvatarGroupItemProps) {
const {
child,
index,
size,
orientation,
dir = "ltr",
reverse = false,
itemCount,
className,
style,
...itemProps
} = props;
const maskStyle = React.useMemo<React.CSSProperties>(() => {
let maskImage = "";
let shouldMask = false;
if (orientation === "vertical" && dir === "rtl" && reverse) {
shouldMask = index !== itemCount - 1;
} else {
shouldMask = reverse ? index < itemCount - 1 : index > 0;
}
if (shouldMask) {
const maskRadius = size / 2;
const maskOffset = size / 4 + size / 10;
if (orientation === "vertical") {
if (dir === "ltr") {
if (reverse) {
maskImage = `radial-gradient(circle ${maskRadius}px at 50% ${size + maskOffset}px, transparent 99%, white 100%)`;
} else {
maskImage = `radial-gradient(circle ${maskRadius}px at 50% -${maskOffset}px, transparent 99%, white 100%)`;
}
} else {
if (reverse) {
maskImage = `radial-gradient(circle ${maskRadius}px at 50% -${maskOffset}px, transparent 99%, white 100%)`;
} else {
maskImage = `radial-gradient(circle ${maskRadius}px at 50% ${size + maskOffset}px, transparent 99%, white 100%)`;
}
}
} else {
if (dir === "ltr") {
if (reverse) {
maskImage = `radial-gradient(circle ${maskRadius}px at ${size + maskOffset}px 50%, transparent 99%, white 100%)`;
} else {
maskImage = `radial-gradient(circle ${maskRadius}px at -${maskOffset}px 50%, transparent 99%, white 100%)`;
}
} else {
if (reverse) {
maskImage = `radial-gradient(circle ${maskRadius}px at -${maskOffset}px 50%, transparent 99%, white 100%)`;
} else {
maskImage = `radial-gradient(circle ${maskRadius}px at ${size + maskOffset}px 50%, transparent 99%, white 100%)`;
}
}
}
}
return {
width: size,
height: size,
maskImage,
};
}, [size, index, orientation, dir, reverse, itemCount]);
return (
<Slot
data-slot="avatar-group-item"
className={cn(
"size-full shrink-0 overflow-hidden rounded-full [&_img]:size-full",
className,
)}
style={{
...maskStyle,
...style,
}}
{...itemProps}
>
{child}
</Slot>
);
}
export { AvatarGroup };Layout
import { AvatarGroup } from "@/components/ui/avatar-group";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
<AvatarGroup>
<Avatar>
<AvatarImage src="/tony-hawk.png" />
<AvatarFallback>TH</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="/rodney-mullen.png" />
<AvatarFallback>RM</AvatarFallback>
</Avatar>
</AvatarGroup>Examples
With Truncation
Automatically truncate long lists and show overflow indicators with the max prop.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { AvatarGroup } from "@/components/ui/avatar-group";
const avatars = [
{
name: "shadcn",
src: "https://github.com/shadcn.png",
fallback: "CN",
},
{
name: "Ethan Niser",
src: "https://github.com/ethanniser.png",
fallback: "EN",
},
{
name: "Guillermo Rauch",
src: "https://github.com/rauchg.png",
fallback: "GR",
},
{
name: "Lee Robinson",
src: "https://github.com/leerob.png",
fallback: "LR",
},
{
name: "Evil Rabbit",
src: "https://github.com/evilrabbit.png",
fallback: "ER",
},
{
name: "Tim Neutkens",
src: "https://github.com/timneutkens.png",
fallback: "TN",
},
{
name: "Delba de Oliveira",
src: "https://github.com/delbaoliveira.png",
fallback: "DO",
},
{
name: "Shu Ding",
src: "https://github.com/shuding.png",
fallback: "SD",
},
];
export function AvatarGroupTruncationDemo() {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Max 3 items</h3>
<AvatarGroup max={3}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Max 5 items</h3>
<AvatarGroup max={5}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
</div>
);
}With RTL
Support for right-to-left layouts and vertical RTL stacking.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { AvatarGroup } from "@/components/ui/avatar-group";
const avatars = [
{
name: "shadcn",
src: "https://github.com/shadcn.png",
fallback: "CN",
},
{
name: "Ethan Niser",
src: "https://github.com/ethanniser.png",
fallback: "EN",
},
{
name: "Guillermo Rauch",
src: "https://github.com/rauchg.png",
fallback: "GR",
},
{
name: "Lee Robinson",
src: "https://github.com/leerob.png",
fallback: "LR",
},
];
export function AvatarGroupRtlDemo() {
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">RTL</h3>
<AvatarGroup dir="rtl">
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse RTL</h3>
<AvatarGroup dir="rtl" reverse>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical RTL</h3>
<div className="flex justify-center">
<AvatarGroup orientation="vertical" dir="rtl">
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical reverse RTL</h3>
<div className="flex justify-center">
<AvatarGroup orientation="vertical" dir="rtl" reverse>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
</div>
</div>
</div>
);
}With Icons
Use the Avatar Group component with icons or other elements beyond avatars.
import { Bell, Heart, MessageCircle, Settings, Star, User } from "lucide-react";
import { AvatarGroup } from "@/components/ui/avatar-group";
const iconData = [
{ icon: User, color: "bg-blue-500" },
{ icon: Heart, color: "bg-red-500" },
{ icon: Star, color: "bg-yellow-500" },
{ icon: MessageCircle, color: "bg-green-500" },
{ icon: Settings, color: "bg-purple-500" },
{ icon: Bell, color: "bg-orange-500" },
];
export function AvatarGroupIconsDemo() {
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Icon Group</h3>
<AvatarGroup>
{iconData.slice(0, 4).map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-10 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={16} />
</div>
);
})}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Icon Group with Truncation</h3>
<AvatarGroup max={3}>
{iconData.map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-10 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={16} />
</div>
);
})}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse Icon Group</h3>
<AvatarGroup reverse>
{iconData.slice(0, 4).map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-10 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={16} />
</div>
);
})}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse with Truncation</h3>
<AvatarGroup reverse max={3}>
{iconData.map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-10 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={16} />
</div>
);
})}
</AvatarGroup>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical Icon Group</h3>
<div className="flex justify-center">
<AvatarGroup orientation="vertical" size={32}>
{iconData.slice(0, 4).map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-8 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={14} />
</div>
);
})}
</AvatarGroup>
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical Reverse Icon Group</h3>
<div className="flex justify-center">
<AvatarGroup orientation="vertical" reverse size={32}>
{iconData.slice(0, 4).map((item, index) => {
const IconComponent = item.icon;
return (
<div
key={index}
className={`flex size-8 items-center justify-center rounded-full text-white ${item.color}`}
>
<IconComponent size={14} />
</div>
);
})}
</AvatarGroup>
</div>
</div>
</div>
);
}API Reference
AvatarGroup
The main avatar group container that handles layout and masking of child elements.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-orientation] | "horizontal" | "vertical" |