Dice UI
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.

PropTypeDefault
orientation?
Orientation
"horizontal"
dir?
Direction
"ltr"
size?
number
40
max?
number
-
reverse?
boolean
false
asChild?
boolean
false
Data AttributeValue
[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>
  );
}