# How to Implement Dialog and AlertDialog with Astryx Accessibility Features

> Learn to implement accessible Dialog and AlertDialog components using Astryx. Astryx ensures proper focus management, labeling, and dismissal for native dialogs.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-06

---

**Astryx provides fully-featured, accessible dialog components that leverage the native `<dialog>` element and follow WAI-ARIA best practices, automatically handling focus management, labeling, and dismissal behaviors.**

The Astryx design system from Meta (facebook/astryx) offers two primary modal solutions: the generic `Dialog` component for standard modal interactions and the specialized `AlertDialog` for critical flows that require explicit user acknowledgment. Both components are engineered to meet WCAG 2.1 AA standards out of the box, with built-in support for screen readers and keyboard navigation.

## Core Dialog Component Architecture

The `Dialog` component in [`packages/core/src/Dialog/Dialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Dialog/Dialog.tsx) serves as the foundation for all modal interactions in Astryx. It wraps the native HTML `<dialog>` element and automatically implements essential accessibility patterns.

### Automatic Accessible Naming with DialogHeader

When you render a `DialogHeader` inside the dialog, Astryx automatically generates an accessible name without additional markup. The header uses React's `useId` hook to create a unique identifier for the title element, which the dialog then references via `aria-labelledby`.

As implemented in [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) lines 83-90, the `attachDialog` callback receives this generated ID and applies it to the dialog element. However, if you provide your own `aria-label` or `aria-labelledby` attributes, the automatic labeling is suppressed via the `hasConsumerName` guard (lines 97-100), ensuring consumer-provided labels always take precedence.

### Purpose-Driven Dismissal Behavior

The `purpose` prop controls which exit mechanisms are available to users, directly impacting the dialog's accessibility characteristics:

- **`info`** (default): Allows both **Escape key** and **backdrop click** dismissal. Use this for informational content that doesn't require user action.
- **`form`**: Allows **Escape key** only; backdrop clicks are ignored. Ideal for forms where accidental data loss should be prevented.
- **`required`**: Disables **both** Escape and backdrop dismissal. The dialog can only be closed programmatically via the `onOpenChange` callback.

These constraints are calculated in [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) lines 27-30, where `allowEscape` and `allowBackdropClick` booleans are derived from the `purpose` value.

### ARIA Roles and Alert Dialog Semantics

When `purpose="required"`, the component conditionally applies `role="alertdialog"` (lines 66-71) to signal to assistive technologies that this is a mandatory interruption requiring immediate attention. For other purposes, the component relies on the native `<dialog>` element's default semantics.

### Focus Management and Keyboard Handling

Astryx implements robust focus management to preserve navigation flow. When opening a dialog, the component captures the currently focused element in `triggerElementRef`. After the dialog closes, focus is automatically restored to that element.

The Escape key handling (lines 92-105) respects IME composition states and active focus-trap logic before checking the `allowEscape` flag. Backdrop dismissal (lines 139-142) verifies that `event.target === event.currentTarget` to ensure clicks on the `::backdrop` pseudo-element trigger close only when `purpose === 'info'`.

### Development Guardrails

In development mode, Astryx emits a console warning if you open a dialog without an accessible name. This check (lines 111-124) ensures you haven't forgotten to include either a `DialogHeader` (which provides automatic labeling) or explicit `aria-label`/`aria-labelledby` attributes.

## AlertDialog for Critical Interactions

`AlertDialog` in [`packages/core/src/AlertDialog/AlertDialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/AlertDialog/AlertDialog.tsx) extends the base dialog with stricter accessibility requirements for destructive or time-sensitive actions.

### Enforcing Required Behavior

Unlike the standard `Dialog`, `AlertDialog` hard-codes `purpose="required"` (lines 118-123), automatically setting `role="alertdialog"` and disabling all passive dismissal methods. Users cannot close the alert by clicking outside or pressing Escape—the only exit path is through explicit interaction with the dialog's action buttons.

This guarantees that users acknowledge critical messages, such as session expiration warnings or destructive operation confirmations, before proceeding.

### Imperative API with useImperativeAlertDialog

For programmatically triggered alerts, Astryx provides the `useImperativeAlertDialog` hook in [`packages/core/src/AlertDialog/useImperativeAlertDialog.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/AlertDialog/useImperativeAlertDialog.tsx). This hook returns a `show(options)` method that accepts title, description, and action configurations, returning a promise-like handle for programmatic closing.

The hook manages internal state to render the dialog on demand (lines 55-73) while maintaining the same accessibility guarantees as the declarative component. When using this API, the alert dialog still automatically wires the accessible name via `DialogHeader` (referenced from [`DialogHeader.tsx`](https://github.com/facebook/astryx/blob/main/DialogHeader.tsx) lines 69-71).

## Implementation Examples

### Basic Dialog with Automatic Labeling

This example demonstrates the automatic `aria-labelledby` wiring when using `DialogHeader`:

```tsx
import {useState} from 'react';
import {Dialog, DialogHeader} from '@astryxdesign/core';

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open dialog</button>

      <Dialog isOpen={open} onOpenChange={setOpen}>
        <DialogHeader title="Settings" onOpenChange={setOpen} />
        <p>Configure your preferences here.</p>
        <button onClick={() => setOpen(false)}>Close</button>
      </Dialog>
    </>
  );
}

```

The `DialogHeader` automatically connects its title to the dialog's `aria-labelledby` attribute.

### Form Dialog (Escape Allowed, Backdrop Disabled)

Use this pattern when you want to prevent accidental backdrop clicks but allow keyboard users to escape:

```tsx
<Dialog
  isOpen={open}
  onOpenChange={setOpen}
  purpose="form"
>
  <DialogHeader title="Edit profile" />
  {/* Form fields */}
</Dialog>

```

### Required Dialog (No Passive Dismissal)

For critical updates where the user must explicitly acknowledge the content:

```tsx
<Dialog
  isOpen={open}
  onOpenChange={setOpen}
  purpose="required"
>
  <DialogHeader title="Critical update" />
  <p>Proceed with the operation; you cannot dismiss this dialog.</p>
  <button onClick={() => setOpen(false)}>Acknowledge</button>
</Dialog>

```

Note that this sets `role="alertdialog"` automatically.

### Imperative Alert Dialog

For emergency notifications that interrupt user workflow:

```tsx
import {useImperativeAlertDialog} from '@astryxdesign/core';

export function AlertDemo() {
  const alert = useImperativeAlertDialog();

  const showAlert = () => {
    alert.show({
      title: 'Session expired',
      description: 'Your session has timed out. Please sign in again.',
      confirmLabel: 'Sign In',
      onAction: () => {/* handle re‑login */},
    });
  };

  return <button onClick={showAlert}>Show alert</button>;
}

```

This alert cannot be dismissed by Escape or backdrop click; the only exit is through the `onAction` callback.

## Summary

- **Astryx Dialog components** leverage the native `<dialog>` element with automatic `aria-labelledby` wiring via `DialogHeader` and `useId` hooks.
- **Purpose-driven behavior** (`info`, `form`, `required`) controls dismissal methods and ARIA roles, with `required` automatically setting `role="alertdialog"`.
- **Focus management** preserves navigation flow by restoring focus to the trigger element after closure.
- **AlertDialog** enforces mandatory acknowledgment by disabling all passive dismissal methods and hard-coding `purpose="required"`.
- **Development warnings** ensure dialogs always have accessible names before reaching production.

## Frequently Asked Questions

### How does Astryx automatically label dialogs for screen readers?

When you include a `DialogHeader` component inside `Dialog` or `AlertDialog`, Astryx generates a unique ID using React's `useId` hook and assigns it to the header's title element. The dialog's `attachDialog` callback then applies this ID to `aria-labelledby` on the dialog element, as seen in [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) lines 83-90. If you provide your own `aria-label` or `aria-labelledby`, these automatic attributes are suppressed to respect your explicit labeling.

### What is the difference between Dialog purpose="required" and AlertDialog?

While both enforce `role="alertdialog"` and disable passive dismissal, `AlertDialog` is a specialized wrapper that hard-codes `purpose="required"` and provides additional imperative APIs via `useImperativeAlertDialog`. The standard `Dialog` with `purpose="required"` is declarative and controlled via props, whereas `AlertDialog` is specifically designed for critical, non-deferrable interruptions with a more restrictive API surface.

### Can users close a Dialog by clicking the backdrop?

Backdrop click dismissal is only permitted when `purpose="info"` (the default). When `purpose="form"`, backdrop clicks are ignored while Escape key functionality remains active. For `purpose="required"` or when using `AlertDialog`, both backdrop clicks and Escape key presses are disabled, forcing users to interact with explicit controls inside the modal.

### How does Astryx handle focus when a dialog closes?

According to the implementation in [`Dialog.tsx`](https://github.com/facebook/astryx/blob/main/Dialog.tsx) lines 41-75, the component captures the element that had focus before opening (stored in `triggerElementRef`). When the dialog closes, focus is programmatically restored to that element, ensuring keyboard and screen reader users return to their original position in the document navigation flow.