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-morph
Live · 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-morph

Adds 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.