WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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.

4000
70
<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

PropTypeDefaultDescription
defaultItemsT[][]Initial items (uncontrolled)
getKey(item: T) => stringDerives a stable unique key from an item
renderItem(item: T) => ReactNodeRenders the content of a single item
onItemsChange(items: T[]) => voidCallback fired when the live (non-ghost) items change
undoTimeoutnumber4000Window during which a ghost item can be undone (ms)
waveIntervalnumber70Wave interval per item in a batch cascade (ms)
removedLabelstring"已移除"Ghost item text
undoLabelstring"復原"Undo button text
emptyStateReactNodeContent shown when the list is empty
refRef<MutationListHandle<T>>Imperative handle
itemClassNamestringAppended to the className of each item container

MutationListHandle

The methods available through ref.current:

MethodSignatureDescription
add(item: T, index?: number) => voidAdds an item: the gap opens first, then the item drops into it
remove(key: string) => voidSoft-deletes a single item with an undo ghost
removeMany(keys: string[]) => voidBatch soft-delete; ghosts and collapse run as a cascading wave
update(key: string, updater: (item: T) => T) => voidUpdates an item's content in place
move(from: number, to: number) => voidMoves by index; the displacement is shown with FLIP
sort(compare: (a: T, b: T) => number) => voidRe-sorts; the displacement is shown with FLIP
shuffle() => voidRandom shuffle
undo() => voidUndoes 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 waits 0.08s before dropping into place — a "gap first, land second" rhythm.
  • Deletion: for the duration of undoTimeout a 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: removeMany assigns each item a wave index in the order it was passed, so the ghost fade-in, countdown, and final collapse are all staggered by waveInterval.

Accessibility

  • The list is marked role="list", and every addition, removal, update, and undo is announced through an aria-live="polite" region.
  • The ghost's undo button, plus the demo's completion toggle and remove buttons, are native button elements — keyboard-focusable with a focus-visible ring.
  • 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.

On this page