@holmdigital/components — Dialogs & Modals
Open source React component. Accessible React Dialog component with focus trapping, ESC close, and WCAG 2.1.2 compliance. No keyboard traps.
The Dialog and Modal components provide accessible, focus-trapping overlays using the native HTML <dialog> element. They handle managing aria-modal, focus restoration, and backdrop interaction automatically.
Regulatory Context
No Keyboard Trap (2.1.2) is a critical "Blocker" issue. Users must always be able to escape a modal (usually ESC key). Ideally, focus should loop within the modal (Focus Trap). Failure to implement this correctly is one of the most severe accessibility violations possible.
Standard Dialog
A non-modal dialog (conceptually) or a standard modal. Our Dialog component uses the native <dialog> element.
Code Example
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} title="Edit Profile" description="Make changes to your profile here." > <YourContent /> </Dialog>Alert Modal
Destructive actions should use the variant="alert" prop to warn users.
Code Example
<Dialog variant="alert" title="Are you sure?" isOpen={isOpen} onClose={close} > <p>This action cannot be undone.</p> <Button variant="danger">Delete</Button> </Dialog>Form in Modal
Modals trap focus correctly, making them safe for complex forms.
Dialog Props
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| isOpen * | boolean | - | Controls visibility. Handles body scroll locking when true. |
| onClose * | () => void | - | Called when the dialog closes (ESC key, backdrop click, or native close event). Must update the isOpen state. |
| title * | string | - | Linked to the dialog container via aria-labelledby. Essential for context. |
| description | string | - | Linked via aria-describedby. Use for additional instructions. |
| variant | 'default' | 'alert' | 'default' | 'alert' produces role="alertdialog" (instead of role="dialog") and a red title. Use for destructive confirmations. |
| initialFocusRef | RefObject<HTMLElement> | - | Element to focus on open. Defaults to the first focusable child. Recommended for destructive confirms (point at Cancel, not Confirm). |
| closeOnBackdropClick | boolean | true | Close when the user clicks the backdrop (the area outside the dialog content). |
| closeOnEscape | boolean | true | Close when the user presses Escape. Set to false for forms with unsaved changes. |
Accessibility Features
- Focus Trap & RestoreTab and Shift+Tab cycle within the dialog. On open, focus moves to
initialFocusRefor the first focusable element. On close, focus is restored to the element that opened the dialog. - Escape to CloseNative dialog Escape handling, with an opt-out via
closeOnEscape={false}for forms with unsaved changes. - Ref-counted Scroll LockBody scroll is locked while open, and stacked dialogs/modals/toasts compose safely — the lock is only released when every requester has released it.
- role="dialog" / role="alertdialog"
variant="alert"upgrades the role toalertdialogfor destructive confirmations.aria-modal="true"is always set. - Unique ARIA Ids per Instance
aria-labelledbyandaria-describedbyuseuseId()so multiple dialogs on the same page never collide. - Backdrop Click via Target IdentityBackdrop clicks are detected via
e.target === dialog(not geometry), so transformed/scaled dialogs and overflowing children behave correctly.
