Components
Stack
A component that arranges elements with overlapping visual effects for avatar groups and more.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Stack } from "@/components/ui/stack";
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 StackDemo() {
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Avatar Stack</h3>
<Stack>
{avatars.slice(0, 4).map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">
Avatar Stack with overflow (max 4)
</h3>
<Stack max={4}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
</div>
);
}
Installation
CLI
npx shadcn@latest add "https://diceui.com/r/stack"
Manual
Install the following dependencies:
npm install @radix-ui/react-slot class-variance-authority
Copy 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 stackVariants = 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 StackProps
extends Omit<React.ComponentProps<"div">, "dir">,
VariantProps<typeof stackVariants> {
size?: number;
max?: number;
asChild?: boolean;
reverse?: boolean;
}
function Stack(props: StackProps) {
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="stack"
{...rootProps}
className={cn(stackVariants({ orientation, dir }), className)}
>
{visibleItems.map((child, index) => (
<StackItem
key={index}
child={child}
index={index}
itemCount={totalRenderedItems}
orientation={orientation}
dir={dir}
size={size}
reverse={reverse}
/>
))}
{shouldTruncate && (
<StackItem
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 StackItemProps
extends Omit<React.ComponentProps<typeof Slot>, "dir">,
VariantProps<typeof stackVariants> {
child: React.ReactElement;
index: number;
itemCount: number;
size: number;
reverse: boolean;
}
function StackItem(props: StackItemProps) {
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="stack-item"
className={cn(
"size-full shrink-0 overflow-hidden rounded-full [&_img]:size-full",
className,
)}
style={{
...maskStyle,
...style,
}}
{...itemProps}
>
{child}
</Slot>
);
}
export { Stack };
Layout
import { Stack } from "@/components/ui/stack";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
<Stack>
<Avatar>
<AvatarImage src="/tony-hawk.png" />
<AvatarFallback>TH</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="/rodney-mullen.png" />
<AvatarFallback>RM</AvatarFallback>
</Avatar>
</Stack>
API Reference
Stack
The main stack container that handles layout and masking of child elements.
Prop | Type | Default |
---|---|---|
orientation? | Orientation | "horizontal" |
dir? | Direction | "ltr" |
size? | number | 40 |
max? | number | - |
reverse? | boolean | false |
asChild? | boolean | false |
Data Attribute | Value |
---|---|
[data-orientation] | "horizontal" | "vertical" |
Examples
With Truncation
Automatically truncate long lists and show overflow indicators with the max
prop.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Stack } from "@/components/ui/stack";
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 StackTruncationDemo() {
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>
<Stack max={3}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Max 5 items</h3>
<Stack max={5}>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
</div>
);
}
With RTL
Support for right-to-left layouts and vertical RTL stacking.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Stack } from "@/components/ui/stack";
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 StackRtlDemo() {
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>
<Stack dir="rtl">
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse RTL</h3>
<Stack dir="rtl" reverse>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical RTL</h3>
<div className="flex justify-center">
<Stack orientation="vertical" dir="rtl">
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical reverse RTL</h3>
<div className="flex justify-center">
<Stack orientation="vertical" dir="rtl" reverse>
{avatars.map((avatar, index) => (
<Avatar key={index}>
<AvatarImage src={avatar.src} />
<AvatarFallback>{avatar.fallback}</AvatarFallback>
</Avatar>
))}
</Stack>
</div>
</div>
</div>
);
}
With Icons
Use the Stack component with icons or other elements beyond avatars.
import { Bell, Heart, MessageCircle, Settings, Star, User } from "lucide-react";
import { Stack } from "@/components/ui/stack";
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 StackIconsDemo() {
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 Stack</h3>
<Stack>
{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>
);
})}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Icon Stack with Truncation</h3>
<Stack 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>
);
})}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse Icon Stack</h3>
<Stack 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>
);
})}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Reverse with Truncation</h3>
<Stack 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>
);
})}
</Stack>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical Icon Stack</h3>
<div className="flex justify-center">
<Stack 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>
);
})}
</Stack>
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="font-medium text-sm">Vertical Reverse Icon Stack</h3>
<div className="flex justify-center">
<Stack 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>
);
})}
</Stack>
</div>
</div>
</div>
);
}