Confirm morph
A destructive button that morphs into an inline confirmation, a spinner, and a result with undo.
actionsnew
Preview
Confirm morph preview
Live · keyboard ready
When to use
- 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.
When not to use
- 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.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "confirm-morph",
"type": "registry:ui",
"title": "Confirm morph",
"description": "A destructive button that morphs into an inline confirmation, a spinner, and a result with undo.",
"categories": [
"actions"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/confirm-morph/confirm-morph.tsx",
"type": "registry:component",
"target": "@components/manicat/confirm-morph/confirm-morph.tsx"
},
{
"path": "registry/components/confirm-morph/confirm-morph.module.css",
"type": "registry:component",
"target": "@components/manicat/confirm-morph/confirm-morph.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/confirm-morph",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/confirm-morph",
"markdown": "https://uiarc.dev/components/confirm-morph/markdown",
"tags": [
"actions",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { MoreHorizontal, Share2, Star, Trash2, Undo2 } from "lucide-react";
import { ConfirmMorph } from "@/components/manicat/confirm-morph/confirm-morph";
export function ConfirmMorphExample() {
return <ConfirmMorph label="Delete" icon={<Trash2 size={16} />} tone="danger" onConfirm={async () => { await new Promise((done) => setTimeout(done, 1200)); }} onUndo={async () => { await new Promise((done) => setTimeout(done, 600)); }} />;
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Delete with undo The button morphs into a confirmation, runs the delete, then offers an undo.
Delete with undo
The button morphs into a confirmation, runs the delete, then offers an undo. preview
Live · keyboard ready
delete-with-undo-the-button-morphs-into-a-confirmation-runs-the-delete-then-offers-an-undo-.tsxtsx
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | Presses the focused face button: start, Confirm, Cancel, Undo, or Retry. In hold mode, holding Enter or Space on Confirm fills it. |
| Enter | While asking, confirms from the question (Confirm takes focus when it opens); during a countdown, runs the action now. |
| Tab / Shift+Tab | Moves between Cancel and Confirm while asking. |
| Escape | Cancels the question or the countdown, or dismisses a result back to rest. |
Accessibility
- The question face is a group labelled by the prompt. Confirm takes focus so Enter answers yes, but a press within 280ms of opening and auto-repeated keys are ignored, so a double click or a held Enter never confirms by accident.
- Focus follows the morph: Confirm while asking, the root during a countdown or while pending (with aria-busy), Undo or Retry on a result, and the trigger back at rest.
- In hold mode the confirm button is described as "Press and hold", and letting go early announces that it needs a longer hold.
- 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.
Motion
- Only the pill's width moves, on the morph spring growing and the smooth spring shrinking, so corners and text never scale or wobble and nothing around it jumps.
- Every face sits at its natural width against the pill's leading edge: the question starts where the label was and the springing edge reveals the rest, so text never squashes, slides, or leaves its baseline.
- The new face's parts arrive in reading order, each clearing a small blur; the old face fades out where it stood. The done check pops and draws itself, and countdown digits swap on tabular figures.
- In hold mode a solid danger copy of the button is revealed from the left as the hold fills and drains back on a spring if released early.
- Reduced motion removes the width spring, blur, and draw; faces crossfade and the spinner slows. The hold fill and countdown ring still show progress.
Responsive behavior
- 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.
Performance
- 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
- 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 confirmMode="hold" for the riskiest deletes and "countdown" when there is no Undo; use dialog when the consequence needs a full explanation.
- Keep the resting button neutral; tone="danger" only colours the confirm answer, and tone="neutral" moves it to the accent for reversible actions.