A live feed where new items spring in at the top and the rest glide down, with leave animations, an item cap, an optional fading tail and a polite announcement for new entries.
- Payment receivedInvoice #1042 · $4,900now
"use client"
import * as React from "react"
import { CreditCard, GitMerge, MessageSquare, UserPlus } from "lucide-react"
import { AnimatedList, AnimatedListItem } from "@/components/ballmac/animated-list"
const events = [
{ icon: CreditCard, title: "Payment received", detail: "Invoice #1042 · $4,900" },
{ icon: UserPlus, title: "New teammate", detail: "Priya joined Design" },
{ icon: GitMerge, title: "Pull request merged", detail: "feat: dark mode for settings" },
{ icon: MessageSquare, title: "New comment", detail: "Marco on “Q4 roadmap”" },
]
type Entry = (typeof events)[number] & { id: number }
export default function AnimatedListDemo() {
const [items, setItems] = React.useState<Entry[]>([{ ...events[0]!, id: 0 }])
React.useEffect(() => {
let n = 1
const id = setInterval(() => {
setItems((list) => [{ ...events[n % events.length]!, id: n++ }, ...list].slice(0, 6))
}, 1600)
return () => clearInterval(id)
}, [])
return (
<div className="h-80 w-full max-w-sm overflow-hidden">
<AnimatedList max={5} fadeEnd announce label="Recent activity" className="gap-2">
{items.map(({ id, icon: Icon, title, detail }) => (
<AnimatedListItem key={id} className="flex items-center gap-3 rounded-xl border bg-card p-3 shadow-xs">
<span aria-hidden="true" className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<Icon className="size-4" />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-foreground">{title}</span>
<span className="block truncate text-xs text-muted-foreground">{detail}</span>
</span>
<span className="ms-auto font-mono text-[11px] text-muted-foreground">now</span>
</AnimatedListItem>
))}
</AnimatedList>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/animated-listInstall the dependencies.
$ pnpm add motion@^12Copy the source into your project.
components/ballmac/animated-list.tsx// Ballmac UI: Animated List. https://ui.ballmac.com/components/animated-list "use client" import * as React from "react" import { AnimatePresence, motion, useReducedMotion } from "motion/react" import { cn } from "@/lib/utils" type AnimatedListProps = Omit<React.ComponentProps<"ul">, "ref"> & { /** Most items drawn. Extra items beyond this are left out. */ max?: number /** Fade the last visible items out so a long feed trails off. */ fadeEnd?: boolean /** Read new items aloud politely, for feeds people rely on. */ announce?: boolean /** Accessible name of the feed. */ label?: string } /** A feed where new items spring in and the rest glide down. Put an AnimatedListItem with a stable `key` for each entry. */ function AnimatedList({ max, fadeEnd = false, announce = false, label, className, children, ...props }: AnimatedListProps) { const items = React.Children.toArray(children) const shown = typeof max === "number" ? items.slice(0, max) : items return ( <ul data-slot="animated-list" aria-label={label} aria-live={announce ? "polite" : undefined} aria-relevant={announce ? "additions" : undefined} className={cn("relative flex flex-col", fadeEnd && "[mask-image:linear-gradient(to_bottom,black_70%,transparent)]", className)} {...props} > <AnimatePresence initial={false} mode="popLayout"> {shown} </AnimatePresence> </ul> ) } type AnimatedListItemProps = Omit<React.ComponentProps<"li">, "ref"> function AnimatedListItem({ className, children, ...props }: AnimatedListItemProps) { const reduce = useReducedMotion() return ( <motion.li data-slot="animated-list-item" layout={reduce ? false : "position"} initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.85, y: -24 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.9, transition: { duration: 0.18 } }} transition={{ type: "spring", stiffness: 380, damping: 32 }} style={{ originY: 0 }} className={cn(className)} {...(props as object)} > {children} </motion.li> ) } export { AnimatedList, AnimatedListItem, type AnimatedListProps, type AnimatedListItemProps }Update the import paths to match your project setup.
Usage
import { AnimatedList, AnimatedListItem } from "@/components/ballmac/animated-list"The full example is in the Code tab above.
Examples
Capped activity log
Deploy log
"use client"
import * as React from "react"
import { AnimatedList, AnimatedListItem } from "@/components/ballmac/animated-list"
const lines = ["Build started", "Installing dependencies", "Compiling 218 modules", "Running tests", "Uploading assets", "Deployment ready"]
export default function AnimatedListLog() {
const [log, setLog] = React.useState<{ id: number; text: string }[]>([])
const [running, setRunning] = React.useState(true)
React.useEffect(() => {
if (!running) return
let n = 0
const id = setInterval(() => {
setLog((l) => [{ id: n, text: lines[n % lines.length]! }, ...l].slice(0, 4))
n++
}, 1200)
return () => clearInterval(id)
}, [running])
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-4">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold">Deploy log</h3>
<button
type="button"
aria-pressed={!running}
onClick={() => setRunning((r) => !r)}
className="h-7 rounded-md border px-2.5 text-xs font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
{running ? "Pause" : "Resume"}
</button>
</div>
<AnimatedList max={4} className="min-h-[7.5rem] gap-1.5 font-mono text-xs">
{log.map((l) => (
<AnimatedListItem key={l.id} className="rounded-md bg-muted/60 px-2.5 py-1.5 text-foreground">
<span className="text-muted-foreground">{String(l.id + 1).padStart(2, "0")}</span> {l.text}
</AnimatedListItem>
))}
</AnimatedList>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
maxMost items drawn. Extra items beyond this are left out. | number | — |
fadeEndFade the last visible items out so a long feed trails off. | boolean | false |
announceRead new items aloud politely, for feeds people rely on. | boolean | false |
labelAccessible name of the feed. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Screen readers | announce turns on a polite live region that reads additions only |
| Reduced motion | Items fade in without moving or scaling |
Use with AI
<AnimatedList max fadeEnd announce><AnimatedListItem key={id}>…</AnimatedListItem></AnimatedList>. Each item needs a stable key. Prepend new entries to your array; layout animation does the rest. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Animated List (@ballmac/animated-list) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Live activity, notification and event feeds
- Chat-like logs where entries arrive over time
Not for
- Static lists (stagger-list)
- Hundreds of rows (virtualize instead)
Registry JSON: https://ui.ballmac.com/r/animated-list.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- shadcn/utils
Pairs well with
Notification Stack
macOS Notification Center cards: collapsed, the newest sits in front with two peeking behind; click or press Enter to spring the stack open. New cards slide in from the top; each can be dismissed.
Activity Feed
A compact actor and action feed with timestamps and an empty state.
Stagger List
A list or grid whose items arrive one after another from any direction, then glide to their new places when filtered, sorted, added or removed.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.