Getting started
Foundations
Integrations
Form19
Display5
Layout4
Navigation4
Overlay8
Feedback4
Motion19
Animated List
A list that animates entrances, exits, and the rows displaced by both.
#Installation
npx @dinachi/cli@latest add animated-list#Usage
import { AnimatedList, AnimatedListItem } from "@/components/ui/animated-list"<AnimatedList dismissed={dismissed}>
{rows.map((row) => (
<AnimatedListItem key={row.id} itemKey={row.id}>
{row.title}
</AnimatedListItem>
))}
</AnimatedList>#Examples
#The three cases
Entrances and exits are the easy half. The third case is what decides whether a live list holds together.
| Case | What happens |
|---|---|
| A row enters | Fades and travels in from the edge given by from. |
| A row leaves | Pulled out of layout flow immediately, so the gap starts closing on the same frame rather than after the exit finishes. |
| A row neither entered nor left | Springs to its new position. A row that shifts up reads as displaced rather than as having been repainted somewhere else. |
#Two exits
A row the reader dismissed and a row the server retracted are the same removal to React and
two different events to the reader. Pass the key of the row the reader closed as
dismissed, in the same update that removes it:
const dismiss = (id: string) => {
setDismissed(id)
setRows((current) => current.filter((row) => row.id !== id))
}It slides out towards the control that was just pressed; everything else collapses in
place. Leave dismissed unset and every removal collapses.
It has to be set at removal rather than read at render, because by the time the row is
leaving it is already gone from the list. The value rides AnimatePresence's custom, the
one channel motion re-reads when it resolves an exit. A captured prop would be stale.
#When to use it
| Use it for | Not for |
|---|---|
| A list the reader is already looking at when it changes: a notification feed, an inbox, a live activity stream. | A list that is simply appearing. Use Stagger List: a list that already has its content wants an entrance, not a reconciliation. |
| Membership that changes a row or two at a time. | A list that replaces its whole contents on every keystroke. Reconciling forty rows at once is a wait, not a transition. |
#Behaviour
itemKeyduplicates React'skeybecause a component cannot read its own key back. It is only needed if the list is usingdismissed.AnimatedListItemuses motion'sx/y/scaleshorthands rather than the full transform strings the rest of the tier prefers.layoutbuilds the element's transform out of those values plus its own projection delta, so atransformstring would fight it.- Opacity runs on a fixed clock while position runs on a spring, so a row is never left half-visible waiting for the spring to settle.
#Accessibility
- The list keeps its
ul/lisemantics. Only the markers are dropped. - Reduced motion drops the travel but not the reflow. Rows still have to end up in the right place; they get there in one frame instead of on a spring. The reflow is not decoration: deleting it would leave the list wrong, not calmer.
- Announce the change yourself if it matters. A list that mutates silently is a motion problem for sighted readers and no signal at all for anyone else.
AnimatedListItemthrows outside anAnimatedList, where nothing would resolve its exit.
#API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| from | "top" | "bottom" | "top" | Which edge new rows arrive from. The direction of travel is a claim about where a row came from |
| dismissed | string | null | null | The itemKey of the row the reader closed. Set it in the same update that removes the row |
| duration | number | 0.3 | Seconds for each row's own animation. The reflow spring is scaled from it |
| itemKey | string | — | On AnimatedListItem. The same string given to React's key, which a component cannot read back |