Confirm morph
A destructive button that morphs into an inline confirmation, a spinner, and a result with undo.
pnpm dlx shadcn@latest add @uiarc/confirm-morphLive · keyboard ready
- Deleting a selection in a table or file list, right where the button sits.
- Revoking access, removing a member, or discarding a draft.
- Actions that should offer Undo in place after they finish.
- Use dialog when the consequence needs more than one line, or when typing a name to confirm is required.
- Use action-button for safe async actions that need no question.
- Use hold-to-confirm when an accidental tap must be nearly impossible.
Installation
Add Confirm morph with the shadcn CLI, or copy the source by hand.
pnpm dlx shadcn@latest add @uiarc/confirm-morphAdds the component and its local dependencies, and installs motion, lucide-react. First time? Add the @uiarc registry to components.json, or use the full URL:
example.tsx
import { Trash2 } from "lucide-react";import { ConfirmMorph } from "@/registry/components/confirm-morph/confirm-morph"; export function DeleteSelection({ ids }: { ids: string[] }) { return ( <ConfirmMorph label="Delete" icon={<Trash2 size={16} strokeWidth={1.75} />} prompt={`Delete ${ids.length} files?`} onConfirm={() => deleteFiles(ids)} onUndo={() => restoreFiles(ids)} /> );}ConfirmMorph
A button for destructive or important actions that morphs into an inline question, then a spinner, then a result with Undo.
PropTypeDefaultDescription
labelRequiredReactNode–The resting label, such as "Delete".iconReactNode–A plain icon before the resting label.promptReactNode–The question while confirming, such as "Delete 3 files?". Defaults to the label with a question mark.confirmLabelstring"Delete"Confirm button label.cancelLabelstring"Cancel"Cancel button label.pendingLabelstring"Deleting"Shown beside the spinner while onConfirm resolves.doneLabelstring"Deleted"Result label.errorLabelstring"Couldn’t finish"Error label.retryLabelstring"Retry"Retry button label on the error face.undoLabelstring"Undo"Undo button label.undoingLabelstring"Restoring"Shown beside the spinner while onUndo resolves.tone"danger" | "neutral""danger"danger colours the resting label and confirm button red; neutral uses the foreground.onConfirm() => void | Promise<unknown>–Runs on confirm. Return a promise to show the pending face; a rejection shows the error face with Retry.onUndo() => void | Promise<unknown>–Adds Undo to the result. Return a promise to show a pending face while it runs.onCancel() => void–Called when the question is cancelled, including by Escape, outside press, or timeout.state"idle" | "confirming" | "pending" | "done" | "error"–Controlled state. Pair it with onStateChange.defaultState"idle" | "confirming" | "pending" | "done" | "error""idle"Starting state when uncontrolled.onStateChange(state: ConfirmMorphState) => void–Called on every state change.confirmTimeoutnumber6000Milliseconds before an unanswered question returns to rest. Hovering pauses it; 0 turns it off.resultTimeoutnumber5000Milliseconds a result stays before returning to rest. Hovering pauses it; 0 turns it off.cancelOnOutsidePressbooleantrueA press outside the control cancels an open question.disabledbooleanfalseDisables the resting button.classNamestring–Class on the root.refRef<HTMLDivElement>–The root element, which also takes focus while the action is pending.- EnterorSpace
- Presses the focused face button: start, Cancel, Confirm, Undo, or Retry.
- TaborShift+Tab
- Moves between Cancel and Confirm while asking.
- Escape
- Cancels the question, or dismisses a result back to rest.
- The question face is a group labelled by the prompt, and Cancel takes focus first so Enter never confirms by accident.
- Focus follows the morph: each new face focuses its main button, and the root holds focus while pending with aria-busy.
- Prompts, pending labels, results, and "Undo is available" are announced through a polite live region.
- Timeouts pause while the pointer rests on the control and while the tab is hidden.
- The surface width springs to each face (a bouncier spring when growing, a critically damped one when shrinking), so nothing around it jumps.
- Faces slide in from the right going forward and from the left going back, with a soft blur; the done check draws itself.
- A thin bar drains along the bottom edge for the timeout.
- Reduced motion removes the width spring, slide, blur, and draw; faces crossfade and the spinner slows.
- The control is as wide as its current face; leave room beside it for the question face, which is wider than the resting button.
- Hover styles apply only on hover-capable fine pointers; the control keeps the small control height, so give Cancel and Confirm some space from nearby targets on touch layouts.
- Only the current face and the one leaving render; a ResizeObserver per face measures width for the spring.
- The timeout is a single linear motion value animation that pauses and resumes, not a timer per frame.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for destructive or important actions that deserve a second press but not a modal: delete, revoke, discard.
- Return the real promise from onConfirm; rejections show the error face with Retry, which reruns the same handler.
- Offer onUndo when the action can be reversed; then consider a short confirmTimeout or skipping the question in your own flow.
- Use hold-to-confirm when a deliberate press and hold fits better, and dialog when the consequence needs a full explanation.
The full library index for assistants is at /llms.txt.