Confirm Button
A button that runs its action on a click, a second click or a press and hold, with optional undo.
// Nothing has run yet
npx shadcn@latest add @sureui/confirm-button
import { ConfirmButton } from "@/components/ui/sureui/confirm-button"<ConfirmButton gesture="click-again" onConfirm={deleteProject}> Delete project</ConfirmButton>gesture picks how people confirm, and undo adds a few seconds to take it back.
Undo
The button turns into Undo right away, and onConfirm waits until the window closes. Undo calls onCancel instead.
// Nothing has run yet
The window lasts 5 seconds, or the milliseconds you pass to undo between 4 and 60 seconds, and pauses while the pointer or focus is on the button. Unmounting during the window drops the action without calling either handler.
With undo="manual", Undo stays until people move on: a press outside the button or focus moving elsewhere runs onConfirm. There is no fill, since nothing is counting down.
Click again
The first click arms the button and shows confirmLabel. The second click confirms.
// Nothing has run yet
The button disarms after timeout or when it loses focus.
Hold
Press and hold until the fill completes, and it confirms. Letting go early cancels.
// Nothing has run yet
Moving off the button or losing focus also cancels.
Slide
gesture="slide" confirms when people drag across the button and let go at the end. Letting go early slides it back and cancels, and scrolling the page up or down still works. Two clicks or taps, or two presses of Enter or Space, also confirm, like click again, so nobody has to drag.
// Nothing has run yet
Pending
Return a promise from onConfirm and a ring in the button's color runs around it until it settles. The ring sits 3 pixels outside the button, so a container that clips its overflow cuts it off. Pass pendingLabel to say what is happening while it runs. If the promise rejects, the button goes back to idle.
Set pendingIndicator to "spinner" to put a spinner in front of the label, or "pulse" to fade the button in and out. Set pendingDelay in milliseconds to wait before showing any of them, so fast actions don't flash. The button keeps the width of its widest label, so it doesn't jump.
// Nothing has run yet
Success
Pass successLabel to show it for 1.5 seconds after the promise resolves. A rejection skips it and shows errorLabel instead, if you set one.
// Nothing has run yet
Errors
By default, when onConfirm throws or rejects, the button goes back to idle and the error reaches your app unchanged. Pass onConfirmError to receive it instead, and errorLabel to show the failure on the button until the next activation, which tries again.
// Nothing has run yet
The error label is announced in the polite live region. Change the text with announcements.error.
Arm delay
armDelay ignores activation for that many milliseconds after the button mounts or arms, so the second click of a double click can't pass through. It helps most inside a dialog or popover, where the button appears under the pointer. Double-click Show below: the second click lands on the new button and does nothing.
// Nothing has run yet
Wait
wait keeps the button disabled for that many milliseconds after it appears, counting down on the button ("Wait 3s"), so people read before they confirm. Screen readers hear "Available in 3 seconds" once. Change the text with waitLabel and announcements.wait.
// Nothing has run yet
Labels
confirmLabel shows while armed (default "Click again"; screen readers hear "Click again to confirm") and undoLabel during the undo window.
// Nothing has run yet
ConfirmButton
Extends the shadcn Button. The table lists the props it adds.
| Prop | Type | Default |
|---|---|---|
| ...buttonProps | Button props | — |
| onConfirm | () => void | Promise<unknown> | required |
| onCancel | () => void | — |
| onConfirmError | (error: unknown) => void | — |
| gesture | "click" | "click-again" | "hold" | "slide" | "click" |
| undo | boolean | number | "manual" | — |
| pauseUndoOnHover | boolean | true |
| pauseUndoOnFocus | boolean | true |
| confirmLabel | ReactNode | "Click again", or "Confirm" for a hold |
| undoLabel | ReactNode | "Undo" |
| errorLabel | ReactNode | — |
| armDelay | number | 0 |
| wait | number | 0 |
| waitLabel | (seconds: number) => ReactNode | (s) => `Wait ${s}s` |
| pendingIndicator | "ring" | "spinner" | "pulse" | "ring" |
| pendingLabel | ReactNode | — |
| pendingDelay | number | 0 |
| successLabel | ReactNode | — |
| timeout | number | 3000 |
| duration | number | 1200 |
| holdFallback | "click-again" | "none" | "click-again" |
| announcements | { armed?, hold?, slide?, fallback?, wait?, undo?, error?, success? } | — |
Data attributes
The button sets data-state, so you can style around it. After onConfirm fails, it also sets data-error until the next activation. While its pending indicator shows, it also sets data-pending.
| Value | When |
|---|---|
| idle | Waiting for input. |
| armed | Clicked once with click-again, or a hold that fell back to a second activation. |
| holding | Being held with gesture="hold". |
| undo | The undo window is open. |
| pending | onConfirm returned a promise that hasn't settled. |
- Key repeat is ignored, so holding Enter can't arm and confirm at once.
- Each state change is announced in a polite live region. Replace the English text with
announcements. - Keyboard users hold Space or Enter, and letting go early cancels. Screen readers send a click that can't be held, so a click with no press arms the button and a second one confirms. Set
holdFallback="none"to turn this off. - With
prefers-reduced-motion, fills jump to their end state instead of moving, the ring and the spinner hold still instead of running, andpulsedims the button instead of pulsing. - While
onConfirmis pending, the button setsaria-busyand keeps focus.successLabelis announced in the same polite live region as the other states.
WCAG
- 2.2.1 Timing Adjustable:
undo="manual"meets it, since Undo has no time limit. A numericundostops at 60 seconds. waitisn't a time limit under 2.2.1: it only delays when confirm unlocks, and never ends anything or drops the action.- 2.5.2 Pointer Cancellation:
gesture="hold"meets it, because releasing early or moving off the button cancels.clickandclick-againrun on the up event. - 2.5.7 Dragging Movements:
gesture="slide"meets it, because two clicks confirm without dragging.