Optimistic Mutation Frame
An optimistic update container that inserts a translucent ghost item immediately, solidifies it into a real one when the server confirms, and shakes it back out on failure while handing the error off to a toast.
npx shadcn@latest add https://webberui.com/r/optimistic-mutation-frame.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<OptimisticMutationFrame />
Installation
npx shadcn@latest add https://webberui.com/r/optimistic-mutation-frame.jsonOr, once registries are configured in components.json, install it as @webberui/optimistic-mutation-frame.
Usage
OptimisticFrame wraps any list or set of cards, and a ref gives you its imperative operation interface. Calling add immediately inserts a translucent ghost item and then waits on the async task you pass in: on success it solidifies into a real item, and on failure it shakes back out and raises a toast.
import * as React from "react";
import {
OptimisticFrame,
type OptimisticFrameHandle,
} from "@/components/ui/optimistic-mutation-frame";
interface Todo {
id: string;
text: string;
}
export function Example() {
const frame = React.useRef<OptimisticFrameHandle<Todo>>(null);
function handleAdd(todo: Todo) {
frame.current?.add(todo, async () => {
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(todo),
});
if (!res.ok) throw new Error("Save failed");
return (await res.json()) as Todo; // the server response overrides the optimistic data
});
}
return (
<OptimisticFrame<Todo>
ref={frame}
defaultItems={seed}
getKey={(todo) => todo.id}
>
{(entry) => (
<div data-pending={entry.pending}>{entry.data.text}</div>
)}
</OptimisticFrame>
);
}The value returned by task (if any) overrides the optimistically inserted data, letting you fill real server-generated fields such as ids and timestamps back into the item. Throwing means failure, which triggers the rollback.
Props
OptimisticFrame
| Prop | Type | Default | Description |
|---|---|---|---|
getKey | (item: T) => string | — | Extracts a stable key from the data |
children | (entry: OptimisticEntry<T>) => ReactNode | — | A render prop; style each entry based on entry.status / entry.pending |
defaultItems | T[] | [] | The initial real items (uncontrolled, read only once) |
layout | "list" | "grid" | "list" | Layout direction |
columns | number | 2 | Number of columns in grid layout |
gap | number | 8 | Spacing between items (px) |
toast | boolean | true | Whether to show the built-in error toast |
toastDuration | number | 4000 | How long before the toast disappears automatically (milliseconds) |
onError | (error, ctx) => void | — | Fires on failure; hook it up to an external toast or reporting |
itemClassName | string | — | Styling appended to each item's frame |
ref | Ref<OptimisticFrameHandle<T>> | — | Gives you the imperative operation interface |
OptimisticFrameHandle (through ref)
| Method | Signature | Description |
|---|---|---|
add | (data, task, options?) => Promise<boolean> | Optimistic insert; task success solidifies it and failure rolls it back, returning whether it was confirmed |
remove | (id, task, options?) => Promise<boolean> | Optimistic removal; success actually removes it and failure restores it with a shake |
reset | (items: T[]) => void | Resets to a new set of real items, clearing every ghost state |
options can carry errorMessage (overriding the toast copy) and position ("start" / "end", where an insert goes).
How it works
- Ghost state: optimistically inserted items are shown at
opacity 0.55with a dashed outline to signal they are not yet confirmed;entry.pendingistrue. - Solidifying: once
taskresolves, opacity and scale converge to the real state and the dashed outline fades out. - Shake and roll back: when
taskrejects, the outline turns red and shakes horizontally once, then an added item is removed or a removed item is restored, while the error message is handed off to the toast. - FLIP repositioning: as items enter and leave, the remaining items spring smoothly into their new positions (
layoutanimation). - Every
setTimeoutis cleared on unmount; if the component has already unmounted when an asynctaskfinishes, no state is updated.
Accessibility
- Ghost items and items being rolled back carry
aria-busy="true", so assistive technology knows they are being processed. - The container is
role="list"and the items arerole="listitem"; passaria-labelto name the list. - The error toast sits in an
aria-live="assertive"region withrole="alert"and comes with a keyboard-focusable close button. - When the user has "reduce motion" enabled, the reveal displacement, the solidifying scale, and the shake are all disabled in favor of instant state switching; static cues such as the ghost translucency and the red outline are still kept.
Async State Slot
An async state slot that switches between the five idle/loading/empty/error/content states with height morphing and shared-element transitions.
Suspense Reveal Queue
A reveal conductor wrapping several loading sections — whichever finishes first joins the queue, reveals follow the author's order with a minimum interval, and late sections catch up automatically.