Capítulo 12 de 51

Chapter 12: Alert Dialog

Core Idea

A modal dialog that interrupts the user and requires an explicit response (confirm/cancel) — same overlay mechanics as Dialog, plus a Backdrop, scrollable Viewport, and an imperative Handle/createHandle() system for wiring triggers that live outside the dialog's own tree (e.g. inside a Menu).

Key Concepts

  • Anatomy: RootTrigger, PortalBackdrop, ViewportPopupTitle, Description, Close.
  • Detached triggers via Handle: AlertDialog.createHandle() returns a Handle<Payload> passed to both Root and any Triggers rendered outside it — lets a Menu item or other unrelated component open the dialog without controlling its state directly. Handle exposes isOpen (readonly), open(triggerId), openWithPayload(payload), close() — all callable only from event handlers/effects, never during render, and only while a Root using that handle is mounted.
  • actionsRef: alternative imperative control scoped to one Root — exposes unmount() (call after an externally-controlled closing animation finishes) and close().
  • initialFocus/finalFocus on Popup: boolean | RefObject | (interactionType) => element|boolean|null. Default: focus moves to first tabbable element, except touch-opened dialogs focus the popup itself (avoids triggering the virtual keyboard). finalFocus default: returns to trigger or previously-focused element.
  • Nested dialogs: Popup/Viewport expose data-nested and data-nested-dialog-open; a --nested-dialogs CSS variable on Popup counts nesting depth. Child dialogs' backdrops don't render, so the parent dialog stays visible cleanly behind the top one.
  • payload: Trigger accepts a payload prop passed through to the dialog when opened via that trigger — useful for "which item triggered this confirm dialog" data without external state.

Code Examples

/* Detached trigger (inside a Menu) controlling an AlertDialog via a Handle */
const dialogHandle = AlertDialog.createHandle();

<Menu.Item render={<AlertDialog.Trigger handle={dialogHandle} />}>Delete</Menu.Item>
<AlertDialog.Root handle={dialogHandle}>
  <AlertDialog.Portal>
    <AlertDialog.Backdrop />
    <AlertDialog.Popup>
      <AlertDialog.Title>Delete item?</AlertDialog.Title>
      <AlertDialog.Close>Cancel</AlertDialog.Close>
    </AlertDialog.Popup>
  </AlertDialog.Portal>
</AlertDialog.Root>
  • What it demonstrates: handle is the idiomatic way to connect a trigger to a dialog when they can't be siblings in the tree (e.g. trigger lives inside a Menu.Popup, dialog needs to render outside it) — avoids manually lifting open/onOpenChange state.

Reference Tables

PartNotable propsNotable data attributes
Rootopen/onOpenChange, defaultOpen, handle, actionsRef, triggerId/defaultTriggerId, onOpenChangeComplete
Triggerhandle, payload, id, nativeButtondata-popup-open, data-disabled
BackdropforceRenderdata-open, data-closed, data-starting-style, data-ending-style
PopupinitialFocus, finalFocusdata-open, data-closed, data-nested, data-nested-dialog-open, data-starting-style, data-ending-style (CSS var --nested-dialogs)
Viewportsame open/nested/animation attributes as Popup
ClosenativeButtondata-disabled

Key Takeaways

  1. Reach for AlertDialog.createHandle() + handle prop whenever the trigger and the dialog can't be siblings — e.g. a delete confirmation triggered from inside a Menu.Item — instead of lifting open/close state manually.
  2. Use Backdrop's forceRender only when a nested-dialog backdrop is being unexpectedly suppressed and you need it visible anyway; the default (no backdrop on nested dialogs) is deliberate for a clean stacked look.
  3. Handle's imperative methods (open, close, openWithPayload) must be called from event handlers or effects — calling them during render is unsupported and silently ignored before a Root mounts.

Connects To

  • ch021 (Dialog): same core anatomy without the interrupt-only semantics; use Dialog for non-critical content, AlertDialog when a response is required.
  • ch027 (Menu): the detached-trigger-in-a-menu pattern shown above.
  • ch008 (Customization): ChangeEventReason values (trigger-press, outside-press, escape-key, close-press, focus-out, imperative-action) usable with eventDetails.cancel()/allowPropagation().