Confirm Dialog

Pro

Focused confirmation modal with 6 variants, 3 sizes, a type-to-confirm guard, single-button acknowledge mode, loading state, custom labels and async support.

</>sv-confirm-dialog confirmdialogoverlayvariant
import { SvConfirmDialogComponent } from '@styloviz-pro/overlays';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Async confirm with loading

Pro

Hand the dialog the work — [action]

Pro

The action itself

Pro

Type-to-confirm guard

Pro

Acknowledge (single button)

Pro

Sizes & primary variant

Pro

When to use

  • Getting explicit confirmation before a destructive or irreversible action.
  • High-risk deletes — use `requireText` so users type a phrase to confirm.
  • Informational acknowledgements — use `hideCancel` for a single-button dialog.
  • For reversible actions, an Undo toast is friendlier than a confirm.

Async states

  • loading

Inputs

NameTypeDefaultDescription
openboolean (two-way)falseControls visibility. Use `[(open)]` for two-way binding.
variantConfirmDialogVariant'danger'Semantic variant — controls icon, icon color, and confirm button style.
sizeConfirmDialogSize'md'Panel size.
titlestring'Are you sure?'Dialog title.
descriptionstring''Descriptive body text.
confirmLabelstring'Confirm'Label for the confirm button.
cancelLabelstring'Cancel'Label for the cancel button.
loadingbooleanfalseShow a loading spinner on the confirm button (e.g. while async action runs).
confirmDisabledbooleanfalseManually disable the confirm button (in addition to loading/requireText).
hideCancelbooleanfalseHide the cancel button for single-action acknowledge dialogs.
showIconbooleantrueShow the built-in variant icon.
iconstring''Override the built-in variant icon with a custom SVG path.
showClosebooleanfalseShow a close (×) button in the top-right corner.
requireTextstring''Require the user to type this text before Confirm is enabled (type-to-confirm guard for high-risk destructive actions). Empty = off. THE COMPARISON IS NOT `===`, AND THAT WAS THE DEFECT. An exact code-point comparison fails for a phrase the user typed correctly: `é` can be one code point (U+00E9) or two (`e` + U+0301), the two render IDENTICALLY, and which one you get depends on the keyboard, the input method and the operating system. So a user who types exactly what is on screen watches the Confirm button stay disabled with nothing to explain why — on a dialog whose whole job is to be passed deliberately. WHAT IS COMPARED: the phrase and the typed text, each NFC-normalized, trimmed, and with internal runs of whitespace collapsed. Only differences the user CANNOT SEE are folded away. Everything visible still counts — accents (`cafe` must not delete `café`) and, by default, case, which stays significant because slowing the user down is the guard's job; `requireTextCaseSensitive` is the way out of that.
requireTextLabelstring''Label shown above the type-to-confirm field.
requireTextCaseSensitivebooleantrueWhether the typed phrase must match the case of `requireText`.
requireTextLocalestring | undefinedundefinedBCP-47 tag for the comparison, e.g. `'tr'`. Defaults to the runtime locale. Consulted ONLY when `requireTextCaseSensitive` is false, because case folding is the one part of this comparison that is language-dependent — and it matters most where it is least likely to be tested: Turkish, Azeri, Lithuanian. A host that localises `requireText` should pass the matching tag.
requireTextMatcherConfirmDialogTextMatcher | nullnullReplace the built-in comparison with the host's own. FOR THE CASE NO NORMALIZATION CAN REACH: a phrase that is a resource NAME, shown translated. The user reads "Rechnungen" and the id is "Invoices"; no amount of case folding or NFC makes those equal, and only the host knows they are the same thing. Both arguments arrive canonical (NFC, trimmed, whitespace collapsed), so the matcher never re-implements that. A matcher that THROWS refuses the phrase — a guard on a destructive action fails closed.
initialFocusConfirmDialogFocus'auto'Which control receives focus on open.
keepOpenOnConfirmbooleanfalseKeep the dialog open when Confirm is pressed, leaving the close to the host.
actionConfirmDialogAction | nullnullThe work to perform when Confirm is pressed, owned by the dialog. WITHOUT THIS THE DIALOG CANNOT ANSWER THREE OF ITS OWN QUESTIONS. `(confirmed)` is a notification: the dialog hands the intent over and never learns what happened. So a rejection went nowhere (— the host had to invent its own error surface, and the showcase demo simply swallowed it), a second click sent the destructive request twice (— `[disabled]` applies on the next change-detection pass, which is after the second click has been dispatched), and there was no retry state at all. GIVE IT THE WORK AND ALL THREE FALL OUT of `createAsyncAction`: first-wins so the double submit is refused rather than queued, a normalized `SvDataError` rendered in place, and a Try-again button — but only when the failure could plausibly succeed again, because a 403 will fail identically and offering a retry for it wastes the user's time twice. `(confirmed)` still emits, once per accepted press, so existing hosts and analytics are unaffected. Wiring BOTH means doing the work twice — pick one. ```html <sv-confirm-dialog [(open)]="confirming" [action]="deleteInvoice" pendingDismissal="abort" (actionFailed)="log($event)" /> ``` ```ts protected readonly deleteInvoice = (req: SvRequest) => this.api.delete(this.id(), req.signal); ```
pendingDismissalSvPendingDismissal'block'May the user dismiss the dialog while the action is in flight? IS THAT NOBODY HAD ANSWERED THIS. Cancel, Escape and the backdrop were all silently blocked by `loading` — a defensible answer, but an undocumented one, and the wrong one for a slow request the user has changed their mind about: the dialog simply stopped responding. `'block'` keeps exactly that behaviour and is the default. `'abort'` aborts the request through its `AbortSignal` and closes, which is only honest when the work actually passes `request.signal` to its transport.
retryLabelstring'Try again'Label for the retry button shown after a failed action.
canClose((reason: SvCloseReason) => boolean | void) | nullnullThe host's own veto over every dismissal path. THE REST OF `pendingDismissal` answered the async half — may the user back out while the action is running — and this is the other one: may they back out AT ALL, right now, for a reason only the host knows. A wizard step that must be acknowledged, a licence the user has to accept, a dialog that is the last thing standing between an operator and an irreversible batch. SAME PRIMITIVE AS MODAL AND DRAWER (`createCloseGuard`), so the three answer it identically: return `false` to refuse, return nothing to have no opinion. A veto is silent — the host refused for its own reasons and owns its own messaging. There is no unsaved-changes prompt here: this dialog holds a decision, not a form. ```html <sv-confirm-dialog [canClose]="mustAcknowledge" /> ``` ```ts protected readonly mustAcknowledge = => this.acknowledged(); ```
closeOnBackdropbooleantrueClose on backdrop click.
closeOnEscbooleantrueClose on the ESC key.
customClassstring''Extra CSS classes merged onto the dialog panel.

Outputs

EventPayload typeDescription
confirmedvoidEmits when user clicks Confirm.
cancelledvoidEmits when user clicks Cancel, presses ESC, or clicks the backdrop/close button.
openedvoidEmitted after the dialog opens (by any means, including `[(open)]`).
closedvoidEmitted after the dialog closes (by any means, including `[(open)]`).
actionFailedSvDataErrorThe `[action]` rejected. The dialog stays open and renders the message.
actionCompletedvoidThe `[action]` resolved. Emitted before the dialog closes itself.
openChangebooleanEmits the new `open` whenever it changes — the output half of `[(open)]`.

Customization & theming

  • Six semantic variants (danger · warning · info · success · primary · neutral) and three sizes (sm · md · lg).
  • Type-to-confirm guard (`requireText`), single-button acknowledge mode (`hideCancel`), optional close (×) button.
  • Custom confirm/cancel labels, custom or hidden icon, and configurable initial focus.
  • Rendered into `document.body` via a portal, so it always covers the full viewport and is never clipped by a parent `overflow`/`transform`/`backdrop-filter`.
  • Backdrop / ESC toggles, async `loading`, manual `confirmDisabled`; scroll-lock with scrollbar-width compensation; `customClass` on the panel.
  • `[canClose]` is a veto over every dismissal path — Cancel, Escape, the backdrop and the × button — and it is told WHICH path it is being asked about, so a host can refuse the backdrop and allow the button. Shared with Modal and Drawer via `createCloseGuard`; there is no unsaved-changes prompt here, because this dialog holds a decision rather than a form.
  • `keepOpenOnConfirm` hands the close back to the host so an async confirm can show its loading state, then close (or stay open and show an error).
  • `[action]` hands the work to the dialog instead: first-wins (a second press is refused, not queued), a normalized error rendered in place, a Try again button only when the failure is retryable, `pendingDismissal` deciding whether the user may back out mid-flight, and `actionCompleted` / `actionFailed` reporting the outcome. One async contract, shared with the rest of the kit — `createAsyncAction` in `@styloviz/core`.

Best practices

  • Do: Name the action on the confirm button ("Delete") instead of generic "OK".
  • Do: For irreversible destructive actions, add `requireText`. Focus already lands on Cancel for you: `initialFocus` defaults to `auto`, which means cancel for the `danger` and `warning` variants and confirm for every other one — so "dialog appeared, press Enter" cannot delete anything.
  • Do: Prefer `[action]` over `(confirmed)` for anything asynchronous. `(confirmed)` is a notification — the dialog hands the intent over and never learns what happened, so a rejection has nowhere to go, a double-click sends the request twice (`[disabled]` applies on the next change-detection pass, which is after the second click), and there is no retry state. With `[action]` the dialog refuses the second press, renders the error in place and offers Try again when the failure is retryable.
  • Do: Use `[canClose]` when the dialog must not be dismissed yet — a step that has to be acknowledged, a licence to accept. Return `false` to refuse and nothing to have no opinion; the veto is silent, because you refused for your own reasons and own the messaging. Same input, same rules, on Modal and Drawer.
  • Do: Answer "may the user back out while it is running?" explicitly with `pendingDismissal`. The default `block` is what the dialog always did; `abort` stops the request through its `AbortSignal` and closes, which is only honest if your action passes `request.signal` to the transport.
  • Do: Set `loading` from inside the `confirmed` handler so the button disables on the next render — the dialog refuses a second Confirm until that render, so a double click or a held Enter emits `confirmed` once. Cancel and × are blocked while loading unless `pendingDismissal="abort"`. Or hand the work to `[action]` and let the dialog do it.
  • Do: For a type-to-confirm phrase that is a translated or aliased NAME, pass `[requireTextMatcher]`: it receives both sides already NFC-normalized, trimmed and whitespace-collapsed, and decides what no normalization can (e.g. accept "Rechnungen" for "Invoices"). A matcher that throws refuses the phrase.
  • Do: Set `keepOpenOnConfirm` for any async confirm — otherwise the dialog closes in the same tick as `confirmed` fires and `loading` never renders. The host then owns the close.
  • Don't: Don't confirm trivial, easily reversible actions — it trains users to click through.

Accessibility

  • `role="alertdialog"` + `aria-modal`, labelled/described via per-instance unique ids; body scroll locks while open.
  • On open, focus moves to the type-to-confirm field (if present) or the configured button and Tab is trapped; on close, focus returns to the trigger.
  • `opened`/`closed` fire for every transition; cancel/close are blocked while busy unless `pendingDismissal="abort"`, in which case the Cancel and × buttons stay enabled and abort the request.
  • If the focused trigger is removed because the action succeeded (the deleted row), focus moves to a surviving neighbour in the same list, then the main landmark — never `<body>`.
  • After a failed `[action]`, focus moves to Try again (or Confirm), because the button the user pressed was swapped or disabled and focus would otherwise fall out of the dialog.
  • A failed `[action]` is announced: the message renders in a `role="alert"` beside the buttons, so a screen reader hears the failure rather than a dialog that simply refused to close.

Feedback on Confirm Dialog

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.