accessible-modal-react: reusable keyboard behavior for React dialogs
TL;DR: Give dialogs consistent keyboard and focus behavior with an unstyled React component your team can adapt to its design system.
- react
- accessibility
- dialog
- testing
- open-source
Release #4 in our open-source series. A confirmation window can look finished while leaving a keyboard user behind it or losing their place when it closes. accessible-modal-react provides reusable dialog behavior for teams building React applications, with the visual design left to the application.
Why it matters
A modal dialog is a window that asks someone to finish or dismiss a task before returning to the page. Its keyboard behavior matters as much as its appearance: opening it should move the active keyboard position inside, Tab should stay among its controls, and closing it should return the user to a useful place.
What it does
- Keeps keyboard users inside the dialog. Opening it moves focus inside, and Tab and Shift+Tab wrap between its controls instead of escaping to the page behind.
- Returns people to where they were. Escape and backdrop clicks close the dialog, and focus is restored when it closes. The application can name the control that should receive it.
- Keeps the page behind it quiet. It locks page scrolling and hides background page elements from assistive technology while the dialog is open.
- What it does not do: supply styles, animations, or visual variants, or coordinate stacks of nested dialogs.
ARIA attributes supply information about a control's purpose and relationships to assistive technology, such as a screen reader. The component sets the dialog role and modal attribute; the application supplies a label, or references to its visible heading and description.
Who it's for
- Frontend engineers who want one tested implementation of dialog behavior for confirmation windows and short forms.
- Design-system maintainers who want to share that behavior across products while keeping their own visual design.
What it checks
The 14 automated tests cover rendering, ARIA wiring, keyboard dismissal, configurable backdrop dismissal, focus movement and restoration, scroll locking, background hiding, render-prop children, and state helpers. The labelled dialog fixture passes an axe-core accessibility smoke check in jsdom, a simulated browser environment.
The package itself is a UI component, not an accessibility scanner. These checks describe its automated test coverage; it does not audit the rest of the application.
Architecture and tech stack
Modal renders through a React portal attached to the document body. Small hooks handle focus, scrolling, Escape, and background visibility. useModal supplies open, close, and toggle state helpers. Applications provide the content and backdrop styles, and can pass refs for the initial focus target and the trigger that receives focus after closing.
The README includes a complete confirmation-dialog example and styling rules. The repository also includes basic and form examples. Escape and backdrop dismissal can each be disabled.
| Technology | Role | Why it was chosen |
|---|---|---|
| React and React DOM | Component state and the portal into the document body | Peer dependencies, so the component uses the application's own React and adds no runtime dependencies |
| TypeScript | Component props and generated declarations | Consumers get typed props and refs; declarations ship with the build |
| tsup | ESM and CommonJS builds | One build step for both module systems |
| Vitest, Testing Library, and jsdom | Automated interaction checks | Tests send key presses to the rendered component and check where focus lands |
| axe-core | Accessibility smoke check of the labelled test fixture | Catches common ARIA mistakes automatically; it does not replace manual screen reader testing |
Key design decisions and trade-offs
The headless design shares interaction behavior without owning colors, layout, or animation. That lets teams use their design system, but they must supply backdrop positioning, visual focus indicators, and responsive styling.
The component supports a single dialog workflow and keeps dismissal configurable. It does not coordinate nested modal stacks. Applications must supply a useful accessible name and decide where initial and restored focus belong.
Under the hood
The focus-trap hook chooses the explicit initial focus target, the first tabbable element, or the dialog container. On Tab from the last control it returns to the first; on Shift+Tab from the first it returns to the last. With no tabbable controls, it keeps focus on the container.
Separate effects restore prior body styles and background ARIA attributes when the dialog closes. This separation makes each behavior inspectable, while the application remains responsible for the contents and the focus targets it provides.
Sharp edges and benchmarks
Compatible dependency updates cleared the high and moderate findings reported during release preparation. The full audit still reports one low-severity esbuild development dependency finding; the production dependency audit reports zero findings. The exact commands and remaining finding are recorded in validation.md.
No performance benchmark or real-browser keyboard walkthrough was run. The automated results describe the jsdom fixtures, not a production application or screen-reader session.
Quick start
Use Node.js 20 or later; this release was validated on Node.js 24.13.0 with React 18.3.1. Clone and build the public source:
git clone https://github.com/jare20895/accessible-modal-react
cd accessible-modal-react
npm ci
npm run typecheck
npm test
npm run build
npm pack
The tested release reports:
Test Files 1 passed (1)
Tests 14 passed (14)
The build produces ESM and CommonJS bundles plus TypeScript declarations. npm pack creates accessible-modal-react-0.1.0.tgz. Install that file in your React application:
npm install /absolute/path/to/accessible-modal-react/accessible-modal-react-0.1.0.tgz
This release distributes source on GitHub. These instructions do not depend on an npm registry publication.
Path to production
Add a browser demo and exercise open, close, Tab, Shift+Tab, and focus return with the application's real controls. Follow that with manual VoiceOver/NVDA testing and a review of labels and visual focus styles.
The backlog tracks a browser demo, alert-dialog examples, and nested-modal handling. Resolve the remaining development dependency finding as an upstream-compatible update becomes available, then repeat the build and interaction tests.
Limits, plainly
That evidence does not establish accessibility compliance for your application. Manual VoiceOver/NVDA checks and a real-browser keyboard walkthrough were not performed. Nested modal stacks, drawers, sheets, and specialized alert dialogs are outside this release. Application teams still need to validate labels, focus order, styling, and assistive-technology behavior in their own interface.
The code is MIT licensed. See the public repository for source, tests, examples, and known issues.