shadimage
UI Beats
uianimated-listsource: ok · refreshed

Animated List

The AnimatedList component streams its children in one at a time, springing each new entry into place and pushing the older ones down until they fall out of the bottom of the list.

npx shadcn add "https://uibeats.com/r/animated-list.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/2/2026, 1:04:22 AM

components/demo/animation/animated-list.tsx
"use client";

import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Children, useEffect, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

interface AnimatedListProps {
  /** One entry per child, emitted in order. */
  children: ReactNode;
  /** Milliseconds between entries arriving. */
  delay?: number;
  /** How many entries stay on screen before the oldest is dropped. */
  max?: number;
  /** Start over once the last child has been emitted. */
  loop?: boolean;
  /** Announce arrivals to screen readers, for genuinely live content. */
  live?: boolean;
  className?: string;
}

/**
 * A feed where entries arrive one at a time, pushing the ones already there
 * down and out of the bottom of the list.
 *
 * Only `max` entries are mounted at once, so a feed that has been running for
 * an hour costs exactly what it did in its first second. The shuffle is a
 * layout animation rather than hand-computed offsets: each surviving entry
 * animates from wherever it was to wherever the new arrival pushed it.
 *
 * Nothing here is real notification content; if you wire it to some, turn on
 * `live` so the arrivals are announced rather than appearing silently.
 */
export function AnimatedList({
  children,
  delay = 1400,
  max = 4,
  loop = true,
  live = false,
  className = "",
}: AnimatedListProps) {
  const entries = Children.toArray(children);
  const prefersReducedMotion = useReducedMotion();
  // Total emitted so far. The newest entry is always `emitted - 1`, which
  // makes it its own stable key however many laps the list has run.
  const [emitted, setEmitted] = useState(1);

  const finished = !loop && emitted >= entries.length;

  useEffect(() => {
    if (entries.length === 0 || finished || prefersReducedMotion) return;
    const timer = setTimeout(() => setEmitted((count) => count + 1), delay);
    return () => clearTimeout(timer);
  }, [emitted, delay, entries.length, finished, prefersReducedMotion]);

  if (entries.length === 0) return null;

  // Reduced motion gets the finished list, not a silent animation: the point
  // of the component is the arrival, and there is no quiet way to show it.
  const visibleEntries = prefersReducedMotion
    ? entries.slice(0, max).map((node, position) => ({ key: position, node }))
    : Array.from(
        { length: Math.min(emitted, max, entries.length) },
        (_, position) => {
          const sequence = emitted - 1 - position;
          return { key: sequence, node: entries[sequence % entries.length] };
        },
      );

  return (
    <div
      role="list"
      aria-live={live ? "polite" : undefined}
      aria-relevant={live ? "additions" : undefined}
      className={cn("flex w-full flex-col gap-2", className)}
    >
      <AnimatePresence initial={false}>
        {visibleEntries.map(({ key, node }, position) => (
          <motion.div
            key={key}
            role="listitem"
            layout={!prefersReducedMotion}
            initial={{ opacity: 0, scale: 0.9, y: -28 }}
            animate={{
              // Older entries fade as they sink, so the list ends in a falloff
              // rather than a hard cut at the last row.
              opacity: 1 - position * 0.18,
              scale: 1,
              y: 0,
            }}
            exit={{ opacity: 0, scale: 0.94, transition: { duration: 0.2 } }}
            transition={{ type: "spring", stiffness: 340, damping: 34 }}
            style={{ transformOrigin: "top center" }}
          >
            {node}
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

export default AnimatedList;

More from UI Beats

Browse all components in this library →