Using Reach UI Dialog: Focus‑Trap, Portal Rendering, and Common Pitfalls
Reach UI Dialog gives a ready‑made, accessible modal that traps focus, handles Escape, and renders via a portal. Learn how to use it, test its behavior, and avoid common pitfalls such as nested dialogs and SSR mismatches.
22 Jun 2026, 09:53 UTC

Why Reach UI Dialog is a Good Choice Right Now
Reach UI’s Dialog component gives you a fully WAI‑ARIA compliant modal that handles focus trapping, Escape‑key dismissal, and overlay clicks out of the box. It renders its content into a portal (by default document.body) so that CSS stacking contexts and z‑index issues are avoided. If you’re building a React app that needs a quick, accessible modal without extra styling, @reach/dialog is a solid choice.
How the Component Works Under the Hood
The component tree looks like this on mount:
ReactTree
├─ Trigger (button)
├─ Dialog (isOpen)
│ ├─ DialogOverlay //
│ └─ DialogContent //
Key behaviors:
- Portal Rendering:
DialogContentis moved into a portal so it sits outside the normal React hierarchy. This prevents it from being clipped by CSS overflow or z‑index rules of parent elements. - Focus Trap: When the dialog mounts, a
focus-trapalgorithm runs. The first tabbable element (or an element withautoFocus) receives focus. Tab navigation cycles only inside the dialog. On unmount, focus returns to the element that triggered the dialog. - Keyboard & Mouse Dismiss: An event listener on
keydownchecks for the Escape key and calls theonDismisscallback. Clicking the overlay (data-reach-dialog-overlay) also triggersonDismissunlesscloseOnOverlayClick={false}is set. - ARIA Roles: By default the dialog uses
role=\"dialog\". Passingtype=\"alertdialog\"changes the role toalertdialog, causing screen readers to announce the content immediately.
Concrete Example: A Minimal Accessible Dialog
Below is a complete, copy‑paste example that you can drop into a fresh Vite or Create‑React‑App project. It demonstrates opening, closing, focus trap, and Escape handling.
/* App.jsx */
import React from "react";
import { Dialog, DialogOverlay, DialogContent } from "@reach/dialog";
import "@reach/dialog/styles.css"; // optional base styles
export default function App() {
const [open, setOpen] = React.useState(false);
return (
setOpen(true)}>Open Dialog
setOpen(false)}
aria-label="Example dialog"
>
{/* The overlay is optional but recommended for focus trapping */}
Dialog Title
This is the dialog content. It will receive focus on open.
setOpen(false)}>Close
);
}
Run the app and verify the following:
- Clicking
Open Dialogopens the modal and focuses theh2element. - Tabbing cycles between the
h2,p, andClosebutton only. - Pressing Escape or clicking outside the content closes the dialog.
- After closing, the focus returns to the
Open Dialogbutton.
Server‑Side Rendering (SSR) Considerations
Because the dialog uses a portal, it requires a mounted DOM. If you render isOpen={true} on the first server render, the portal will not exist when React hydrates on the client, leading to a mismatch error. The safest pattern is:
<Dialog isOpen={false} onDismiss={...}>
...
</Dialog>
Then toggle isOpen to true after the component mounts.
Styling the Overlay and Content
Reach UI provides no default styles beyond the reach-dialog-overlay and reach-dialog-content classes. Developers must provide their own CSS for .reach-dialog-overlay, .reach-dialog-content, and focus states, typically using data-reach-* attributes as selectors.
.reach-dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
}
.reach-dialog-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 1rem;
border-radius: 4px;
}
Limitations & Common Mistakes
- Maintenance Mode: The Reach UI team has moved to maintenance only. No new features are added, and the project recommends migrating to Radix UI or Headless UI for future work.
- Nested Dialogs: Opening a second
Dialogwhile another is open breaks focus management and ARIA roles. If you need nested modals, consider a different library. - Overlay Clicks: The overlay click handler relies on
event.target === event.currentTarget. Wrapping the overlay in additional elements can prevent the click from reaching the handler, so keep the overlay as the outermost element in the portal. - Legacy Browser Support: Focus trapping uses a
MutationObserverpolyfill for IE11/Edge. Ensure the polyfill is loaded if you must support those browsers. - TypeScript Casting: The component’s generic props are limited. When using custom render props, you may need to cast
unknownto the expected type. - SSR Hydration Mismatch: As noted, always start with
isOpen={false}on the server to avoid hydration errors.
Testing the Dialog for Accessibility
- Run
npm run devto start the dev server. - Open the page in Chrome, Firefox, or Edge.
- Use the keyboard: Tab to the
Open Dialogbutton, press Enter, and observe focus trap. - Press Escape to close; confirm focus returns.
- Run
npx axe-coreor Lighthouse audit to verify that the dialog hasrole="dialog"and that focus order is correct.
Conclusion
Reach UI’s Dialog component offers a quick, accessible modal solution with automatic focus trapping and keyboard handling. While it’s in maintenance mode and lacks support for nested dialogs, it remains a viable choice for projects that need a lightweight, WAI‑ARIA compliant modal now. For long‑term projects or advanced use cases, consider migrating to Radix UI or Headless UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.