Mutation List Choreographer
Full CRUD list choreography — insertions open a gap and drop in, deletions collapse the neighbours, reordering moves continuously with FLIP, batches cascade in waves, and removals leave an undo ghost.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/mutation-list-choreographer.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<MutationListChoreographer />
Installation
npx shadcn@latest add "https://webberui.com/r/mutation-list-choreographer.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/mutation-list-choreographer.
Usage
The component is uncontrolled: give it initial data through defaultItems, then grab the imperative handle (MutationListHandle) via ref to drive additions, removals, updates, reordering, and undo from anywhere.
import * as React from "react";
import {
MutationListChoreographer,
type MutationListHandle,
} from "@/components/ui/mutation-list-choreographer";
interface Task {
id: string;
title: string;
}
export function Example() {
const listRef = React.useRef<MutationListHandle<Task>>(null);
return (
<>
<button
onClick={() =>
listRef.current?.add({ id: crypto.randomUUID(), title: "New task" })
}
>
Add
</button>
<MutationListChoreographer
ref={listRef}
defaultItems={[{ id: "1", title: "Proofread the release notes" }]}
getKey={(task) => task.id}
renderItem={(task) => <span>{task.title}</span>}
/>
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultItems | T[] | [] | Initial items (uncontrolled) |
getKey | (item: T) => string | — | Derives a stable unique key from an item |
renderItem | (item: T) => ReactNode | — | Renders the content of a single item |
onItemsChange | (items: T[]) => void | — | Callback fired when the live (non-ghost) items change |
undoTimeout | number | 4000 | Window during which a ghost item can be undone (ms) |
waveInterval | number | 70 | Wave interval per item in a batch cascade (ms) |
removedLabel | string | "已移除" | Ghost item text |
undoLabel | string | "復原" | Undo button text |
emptyState | ReactNode | — | Content shown when the list is empty |
ref | Ref<MutationListHandle<T>> | — | Imperative handle |
itemClassName | string | — | Appended to the className of each item container |
MutationListHandle
The methods available through ref.current:
| Method | Signature | Description |
|---|---|---|
add | (item: T, index?: number) => void | Adds an item: the gap opens first, then the item drops into it |
remove | (key: string) => void | Soft-deletes a single item with an undo ghost |
removeMany | (keys: string[]) => void | Batch soft-delete; ghosts and collapse run as a cascading wave |
update | (key: string, updater: (item: T) => T) => void | Updates an item's content in place |
move | (from: number, to: number) => void | Moves by index; the displacement is shown with FLIP |
sort | (compare: (a: T, b: T) => number) => void | Re-sorts; the displacement is shown with FLIP |
shuffle | () => void | Random shuffle |
undo | () => void | Undoes the most recent deletion |
getItems | () => T[] | Reads the current live items |
How it works
- Insertion: the outer row opens a gap by animating height from
0 → auto, and the inner content waits0.08sbefore dropping into place — a "gap first, land second" rhythm. - Deletion: for the duration of
undoTimeouta ghost item covers the original row and offers an undo button plus a countdown progress bar; only after the timeout does the row actually unmount, with neighbours collapsing in to fill the space. - Sorting / moving / shuffling: only the underlying array is reordered; positional changes are handed to Motion's
layout(FLIP) for continuous movement. - Batches:
removeManyassigns each item a wave index in the order it was passed, so the ghost fade-in, countdown, and final collapse are all staggered bywaveInterval.
Accessibility
- The list is marked
role="list", and every addition, removal, update, and undo is announced through anaria-live="polite"region. - The ghost's undo button, plus the demo's completion toggle and remove buttons, are native
buttonelements — keyboard-focusable with afocus-visiblering. - When the user has "reduce motion" enabled at the system level, the gap opening, drop-in, FLIP, cascade, and countdown are all disabled; only the instant state change remains, and the DOM structure is unchanged.
Streak Flame Counter
A Duolingo-style streak flame — the flame flares when the number goes up and changes color at milestones
Filter Composer Bar
A Linear-style filter composer — pick a field from "Add filter" and the pill chip springs in; the chip's value opens a menu in place, and removing one lets the rest close ranks with FLIP.