Inline two-step confirmation on the button itself, with no modal and no dialog state to wire up: the first click arms it — the label swaps to "Confirm?" and the button turns destructive — and only the second click calls onConfirm. It disarms itself after a timeout (3s by default) and on blur, so a stray double-click, a scroll away, a Tab out or a click somewhere else can never fire the action; that pair of escapes is the whole difference between this and the naive version, which is armed forever and goes off the next time the row is clicked for any reason at all. Reach for it wherever a modal would outweigh the action, which is almost every destructive control that lives inside a list rather than on a page: a delete, remove, archive or discard button in a table row, a card, a kanban card, a list item, a file or media row, a comment or post, a cart line, an uploaded attachment, a saved filter or view, a tag or label, or a toolbar; removing a member, collaborator, invite or seat; revoking an API key, token, session or device; deleting a webhook, cron job, alert rule or integration; disconnecting an OAuth app; clearing a cache, a log or a draft; unsubscribing; leaving a channel or workspace; resetting one setting back to its default. Common asks it answers: "confirm before delete without a dialog", "two-step delete button react", "click twice to confirm", "double click to delete", "are you sure button shadcn", "shadcn confirm button", "inline confirmation button", "confirm action without modal", "destructive button with confirmation", "delete button in a table row", "delete confirmation table row react", "undo-less delete confirmation", "replace window.confirm react", "react-confirm-alert alternative", "sweetalert alternative shadcn", "useConfirm hook alternative". Props: `onConfirm` (fired only on the confirming click), `confirmText` for the armed label, `timeout` in milliseconds, plus everything else a `<button>` takes. It is one real `<button>` element — Enter and Space confirm it, `disabled` is honoured, your `className` is merged through `cn`, and it defaults to `type="button"` so an armed click inside a form cannot submit it by accident. The armed state is exposed as `data-armed` for styling and announced through a permanently mounted polite live region, so the change is never carried by colour and a label swap alone — which is exactly what a screen reader would otherwise miss, since nothing about the second click announces itself. Theme-aware through the destructive tokens, zero dependencies (no Radix, no portal, no icon package, one file), and `"use client"` because it holds a state and a timer. What official shadcn/ui offers instead is alert-dialog: a modal that pulls in @radix-ui/react-alert-dialog, renders through a portal, traps focus and needs open state wired up — right for a page-level, consequential confirmation, and far too heavy to put on all twenty rows of a table. Official button has a destructive variant but no confirmation behaviour at all, and official alert is a static message. For an action severe enough that a second click is not enough — deleting a production project, a database or an account — use type-to-confirm, which makes the user type the name first. Composes with loading-button when the confirmed action is slow, and sits naturally next to bulk-action-bar, which confirms the same way for a whole selection rather than one row.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/confirm-button.json"