Velora UI
All components

Animated List

Looping feed that springs each item in, newest on top.

0.7 KB gzippedDeps: motion/reactReduced-motion safeBase UI & Radix compatible
Deploy succeeded

Installation

npx shadcn@latest add https://velora.colorlib.com/r/animated-list.json

Installs with: motion

Source

"use client";

import React, { useEffect, useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";

import { cn } from "@/lib/utils";

interface AnimatedListProps {
  children: React.ReactNode;
  className?: string;
  /** ms between items appearing */
  delay?: number;
}

/**
 * Reveals children one by one, newest on top, then loops.
 * Wrap in a fixed-height container with a bottom mask for a feed effect.
 */
export function AnimatedList({
  children,
  className,
  delay = 2000,
}: AnimatedListProps) {
  const [index, setIndex] = useState(0);
  const items = useMemo(() => React.Children.toArray(children), [children]);

  useEffect(() => {
    const interval = setInterval(
      () => setIndex((i) => (i + 1) % items.length),
      delay
    );
    return () => clearInterval(interval);
  }, [items.length, delay]);

  const visible = useMemo(
    () => items.slice(0, index + 1).reverse(),
    [items, index]
  );

  return (
    <div
      data-slot="animated-list"
      className={cn("flex flex-col gap-3", className)}
    >
      <AnimatePresence>
        {visible.map((item) => (
          <motion.div
            key={(item as React.ReactElement).key}
            layout
            initial={{ scale: 0, opacity: 0 }}
            animate={{ scale: 1, opacity: 1, originY: 0 }}
            exit={{ scale: 0, opacity: 0 }}
            transition={{ type: "spring", stiffness: 350, damping: 40 }}
            className="w-full"
          >
            {item}
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}