How to Implement Dialog and AlertDialog with Astryx Accessibility Features

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 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 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 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 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. 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 lines 69-71).

Implementation Examples

Basic Dialog with Automatic Labeling

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

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:

<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:

<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:

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →