Animated list
Free · MITNotification stack where new items spring in from the top, older ones glide down, slightly dimmed in the depth variant, and items beyond the limit blur away.
Installation
$ npx shadcn@latest add https://goodui.dev/r/animated-list.jsonThe file lands in components/goodui/animated-list.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/animated-list works too.
Usage
import * as React from "react"
import { CalendarCheck } from "lucide-react"
import { AnimatedList } from "@/components/goodui/animated-list"
type Activity = { id: string; client: string; service: string; time: string }
export function BookingFeed({ activity }: { activity: Activity[] }) {
// activity is newest first: prepend new events, e.g. setActivity((a) => [event, ...a])
return (
<AnimatedList
items={activity}
getKey={(a) => a.id}
maxItems={4}
depth
label="New bookings"
renderItem={(a) => (
<div className="flex items-center gap-3 rounded-xl border bg-card p-3 shadow-sm">
<CalendarCheck className="size-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true" />
<div className="min-w-0 flex-1 text-sm">
<p className="truncate font-medium">{a.client} booked {a.service}</p>
<p className="text-xs text-muted-foreground">{a.time}</p>
</div>
</div>
)}
/>
)
}When to use
- Activity feeds and notification panels where new events arrive at the top: new bookings, payments, sign-ups.
- A compact "recent payments" or "latest orders" card that shows the last few items and drops older ones.
- Landing-page demos that simulate live activity by adding an item every few seconds.
- Temporary messages that confirm an action and then disappear: use toast-stack.
- Tabular data with columns, sorting or removal in the middle: use table-row-presence.
- Lists people reorder by hand: use list-reorder.
| If you need | Use |
|---|---|
| Plain feed inside a card | defaults (no depth, no fadeEdge) |
| Notification-stack look, older items slightly smaller and dimmer | depth |
| List that visually trails off at the bottom | fadeEdge |
Examples
Recent payments card with a faded edge
<AnimatedList
items={payments}
getKey={(p) => p.id}
maxItems={5}
fadeEdge
label="Recent payments"
className="gap-0 rounded-xl border bg-card px-3 py-1"
renderItem={(p) => (
<div className="flex justify-between border-b py-2.5 text-sm">
<span>{p.client}</span>
<span className="tabular-nums">+${p.amount}</span>
</div>
)}
/>API reference
| Prop | Type | Default |
|---|---|---|
| items* Newest first. New items enter at the top and push the rest down. | T[] | — |
| getKey* Stable unique key of an item; a new key is what makes an item animate in. | (item: T) => string | — |
| renderItem* Content of one item; `index` is 0 for the newest. | (item: T, index: number) => React.ReactNode | — |
| maxItems Items beyond this count leave the bottom of the stack. | number | 5 |
| depth Dim and shrink older items slightly, like a notification stack. Dimming lowers text contrast, so use text-foreground/75 rather than text-muted-foreground for secondary text. | boolean | false |
| fadeEdge Fade the bottom edge out. | boolean | false |
| label Accessible name for the live region. | string | "Notifications" |
Also accepts every <ul> attribute.
Accessibility
- The list is a <ul aria-live="polite" aria-relevant="additions"> (a real list, so its <li> items stay valid), so screen readers read new items without interrupting.
- label names the list (default "Notifications"); set it to what the feed is, e.g. "Recent payments".
- With depth, older items are dimmed; use text-foreground/75 instead of text-muted-foreground for their secondary text so it keeps 4.5:1 contrast.
- Keep each item's text short: the whole new item is what gets announced.
Motion
- New items enter from 24px above, scaled to 0.92 and blurred 4px, on a spring (stiffness 380, damping 32, mass 0.9); the items below glide down with a layout animation.
- Items pushed past maxItems fade, shrink to 0.9 and blur out over 0.2 s.
- depth: each older item is 6% more transparent and 2% smaller, up to four steps.
- Reduced motion: items appear and disappear without movement, scale or blur, with no transition.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- items is newest first: add new entries with [item, ...items], not push, or they appear at the bottom.
- getKey must be stable and unique per item (an event id); index keys make every item re-animate.
- maxItems only limits what is shown (default 5); trim the array yourself if it can grow without bound.
- renderItem returns the whole card; the list adds no styling beyond a vertical gap-2, which className can override.
- Do not wrap it in another aria-live region; it already is one.
FAQ
How do I simulate live activity on a landing page?
Prepend an item to the array on an interval (setInterval in useEffect, cleared on unmount). The list animates the rest.
Why do new items appear at the bottom?
The list expects newest first. Prepend new items instead of pushing them.
Related
Self-contained toaster with toast() API: cards stack and expand on hover, swipe away with a drag, morph from loading to success or error for promises, and pause their timers on hover.
Count badge for icon buttons that springs in, bumps when the count rises, rolls its digits, caps at 99+ and offers a pulse ring or dot variant.
Data table where new rows grow in with a brief highlight, removed rows slide out and collapse, and re-sorting glides every row to its new position.
Drag-to-reorder list with a grip handle, a lifted shadow on the dragged row, neighbours that spring into place, and Alt+Arrow keyboard moves announced to screen readers.