Display

Animated List

A flex column that staggers items in on first mount and pops, exits and re-flows them as children are added or removed.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { cn } from "@/lib/utils"

export interface AnimatedListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
  /** Each child renders as one list item — every child must carry a stable `key`. */
  children: React.ReactNode
  /** Stagger (ms) applied to each item's entrance delay on first mount; items added later pop in immediately with no extra delay. */
  stagger?: number
}

/**

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/animated-list.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript "AnimatedList" component using the `motion` library
(AnimatePresence + motion.div).

Contract
- Export a forwardRef<HTMLDivElement, AnimatedListProps> where:
  children: ReactNode   required — each child is one list item and must carry
                        a stable `key` (the caller owns identity, this
                        component never generates one)
  stagger?: number      ms of entrance delay multiplied by index on first
                        mount only (default 80)
  className?: string
- No other props — this is a pure animation wrapper, it has no opinion about
  what a "list item" looks like.

Behavior
- Extract items via Children.map + isValidElement, wrap each in a motion.div
  keyed by the child's own key.
- Track whether the component has completed its first mount in a ref (set
  inside a useEffect, not during render). While that ref is still false,
  each item's entrance transition gets `delay = index * stagger` — this is
  the one-time staggered reveal. Once the ref flips true, every item added
  afterwards gets delay 0 and pops in immediately, no matter its index.
- Entrance: initial opacity 0 / y +12 / scale 0.97 → animate to opacity 1 /
  y 0 / scale 1 (spring transition).
- Exit: the reverse of the entrance target (opacity 0 / y +12 / scale 0.97),
  driven by AnimatePresence so removed items play their own exit instead of
  vanishing instantly.
- Use AnimatePresence mode="popLayout" so an exiting item is pulled out of
  layout flow immediately — the remaining items animate into their new
  position (via the `layout` prop) at the same time the removed item is
  still fading out, not after it finishes.
- prefers-reduced-motion (motion's useReducedMotion hook): every item mounts
  directly at its final state (initial={false}), transitions collapse to
  duration 0, and `layout` is disabled — items appear/disappear instantly
  with no animated re-flow, but add/remove still works.

Rendering & styling
- The container itself carries only "flex flex-col gap-2" — no color, no
  border, no radius. It has zero visual opinion; className merges in via
  cn() so the consumer's spacing/width wins.
- No semantic tokens are needed inside this component since it renders no
  chrome of its own — all token usage belongs to whatever the caller passes
  as children.

Customization levers
- Direction & distance: swap the y offset for x, or change 12/0.97 to make
  the entrance travel further or scale more dramatically.
- Spring feel: stiffness/damping on the transition — lower stiffness or
  higher damping reads calmer, higher stiffness reads snappier.
- Stagger: the `stagger` prop controls only the first-mount reveal; tune it
  down for dense lists (long staggers on 20 items feel sluggish) or up for a
  hero-moment reveal of 3-4 items.
- First-play trigger: pair with use-intersection-observer to delay the
  initial stagger until the list scrolls into view, instead of firing on
  mount.
- Layout: drop the `layout` prop if items never resize/reflow and you want
  to save the extra FLIP measurement pass.

Concepts

  • First-play stagger, not a global stagger — the entrance delay only applies while a ref tracking "have we mounted yet" is still false; it's read once per item at render time, so items added a second later never inherit the original index-based delay.
  • AnimatePresence popLayout — without it, a removed item stays in normal flow until its exit animation finishes, so the rest of the list only shifts up after the gap already closed. popLayout removes it from flow immediately so the exit and the re-flow read as one motion.
  • Layout re-flow — the layout prop on each item is what makes the remaining rows glide into their new position instead of snapping there the instant a sibling is removed or inserted.
  • Content-agnostic wrapper — the component owns no visual styling and imports no data shape; it only orchestrates enter/exit/reflow around whatever children the caller passes, each keyed by the caller.
  • Reduced-motion is a real mode, not a skip — under prefers-reduced-motion, items still mount and unmount correctly (state stays truthful), they just do so instantly instead of animating.

On This Page