Confirm morph

A destructive button that morphs into an inline confirmation, a spinner, and a result with undo.

actionsnew
Preview
Confirm morph preview

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

Keyboard interactions

KeysAction
Enter / SpacePresses the focused face button: start, Confirm, Cancel, Undo, or Retry. In hold mode, holding Enter or Space on Confirm fills it.
EnterWhile asking, confirms from the question (Confirm takes focus when it opens); during a countdown, runs the action now.
Tab / Shift+TabMoves between Cancel and Confirm while asking.
EscapeCancels 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.