# Stagger List

The StaggerList component animates its children into view one after another when the list enters the viewport, without requiring any changes to the children themselves.

- **Category:** Animation
- **Docs:** https://uibeats.com/docs/animation/stagger-list
- **Registry:** https://uibeats.com/r/stagger-list.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/stagger-list.json
```

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

For any group that would otherwise arrive as a single slab: feature bullets, pricing tiers, navigation items. The stagger implies sequence and gives the eye somewhere to start. Drop stagger below 0.08s once the list runs past six items, or the last row arrives noticeably late.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | `-` | Items to stagger; each direct child is wrapped (required) |
| `stagger` | `number` | `0.08` | Seconds between each child's entrance |
| `delay` | `number` | `0` | Seconds before the first child animates |
| `duration` | `number` | `0.5` | Duration of each child's entrance, in seconds |
| `distance` | `number` | `24` | Travel distance of the entrance, in pixels |
| `direction` | `'up' \| 'down' \| 'left' \| 'right'` | `'up'` | Direction the children travel from |
| `once` | `boolean` | `true` | Animate only the first time the list enters the viewport |
| `className` | `string` | `''` | Additional CSS classes for the container |

## Example usage

```tsx
"use client";
import { StaggerList } from "@/components/demo/animation/stagger-list";

const StaggerListUsage = () => {
  return (
    <StaggerList stagger={0.1} className="space-y-2">
      {["Design", "Build", "Animate", "Ship"].map((label) => (
        <div
          key={label}
          className="rounded-lg border bg-card px-4 py-2 text-sm"
        >
          {label}
        </div>
      ))}
    </StaggerList>
  );
};

export default StaggerListUsage;
```

## Source

Save as `components/ui/stagger-list.tsx`.

```tsx
"use client";

import { useRef, type ReactNode } from "react";
import {
  motion,
  useInView,
  useReducedMotion,
  type Variants,
} from "motion/react";

interface StaggerListProps {
  children: ReactNode;
  /** Seconds between each child's entrance. */
  stagger?: number;
  /** Seconds before the first child animates. */
  delay?: number;
  /** Duration of each child's entrance, in seconds. */
  duration?: number;
  /** Travel distance of the entrance, in pixels. */
  distance?: number;
  direction?: "up" | "down" | "left" | "right";
  /** Animate only the first time the list enters the viewport. */
  once?: boolean;
  className?: string;
}

const offsetFor = (direction: StaggerListProps["direction"], d: number) => {
  switch (direction) {
    case "down":
      return { y: -d };
    case "left":
      return { x: d };
    case "right":
      return { x: -d };
    default:
      return { y: d };
  }
};

/**
 * Animates its children into view one after another. Each direct child is
 * wrapped, so any markup can be staggered without changing the child itself.
 */
export function StaggerList({
  children,
  stagger = 0.08,
  delay = 0,
  duration = 0.5,
  distance = 24,
  direction = "up",
  once = true,
  className = "",
}: StaggerListProps) {
  const ref = useRef<HTMLDivElement>(null);
  const isInView = useInView(ref, { once, amount: 0.15 });
  const prefersReducedMotion = useReducedMotion();

  /*
   * The stagger goes too, not just the travel. A long list under reduced
   * motion would otherwise still reveal itself one row at a time over several
   * seconds, which is the same distraction the setting asks to be spared.
   */
  const container: Variants = {
    hidden: {},
    visible: prefersReducedMotion
      ? {}
      : { transition: { staggerChildren: stagger, delayChildren: delay } },
  };

  const item: Variants = prefersReducedMotion
    ? {
        hidden: { opacity: 1, x: 0, y: 0 },
        visible: { opacity: 1, x: 0, y: 0 },
      }
    : {
        hidden: { opacity: 0, ...offsetFor(direction, distance) },
        visible: {
          opacity: 1,
          x: 0,
          y: 0,
          transition: { duration, ease: [0.21, 0.47, 0.32, 0.98] },
        },
      };

  return (
    <motion.div
      ref={ref}
      className={className}
      variants={container}
      initial="hidden"
      animate={isInView ? "visible" : "hidden"}
    >
      {/* Array.map over children rather than cloneElement, so children keep
          their own props and refs untouched. */}
      {Array.isArray(children) ? (
        children.map((child, index) => (
          <motion.div key={index} variants={item}>
            {child}
          </motion.div>
        ))
      ) : (
        <motion.div variants={item}>{children}</motion.div>
      )}
    </motion.div>
  );
}

export default StaggerList;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
