Skip to content

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

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

Press and hold, or activate twice, to confirm

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

Slide to the end, or activate twice, to confirm

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

PropTypeDefault
...buttonPropsButton props—
onConfirm() => void | Promise<unknown>required
onCancel() => void—
onConfirmError(error: unknown) => void—
gesture"click" | "click-again" | "hold" | "slide""click"
undoboolean | number | "manual"—
pauseUndoOnHoverbooleantrue
pauseUndoOnFocusbooleantrue
confirmLabelReactNode"Click again", or "Confirm" for a hold
undoLabelReactNode"Undo"
errorLabelReactNode—
armDelaynumber0
waitnumber0
waitLabel(seconds: number) => ReactNode(s) => `Wait ${s}s`
pendingIndicator"ring" | "spinner" | "pulse""ring"
pendingLabelReactNode—
pendingDelaynumber0
successLabelReactNode—
timeoutnumber3000
durationnumber1200
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.

ValueWhen
idleWaiting for input.
armedClicked once with click-again, or a hold that fell back to a second activation.
holdingBeing held with gesture="hold".
undoThe undo window is open.
pendingonConfirm 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, and pulse dims the button instead of pulsing.
  • While onConfirm is pending, the button sets aria-busy and keeps focus. successLabel is 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 numeric undo stops at 60 seconds.
  • wait isn'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. click and click-again run on the up event.
  • 2.5.7 Dragging Movements: gesture="slide" meets it, because two clicks confirm without dragging.