Return Focus to the Trigger with Reach UI Dialog
Reach UI's Dialog traps focus inside a modal and returns it to the trigger element on close, but it requires its CSS bundle and a document.body portal.
16 Jun 2026, 19:53 UTC

The problem: focus disappears when the modal closes
You open a modal, press Escape, and keyboard focus lands on the document body instead of the button that opened it. A screen-reader user may not know the modal is open at all unless you add ARIA attributes by hand. This is a common accessibility regression in custom modals.
The useful takeaway is that Reach UI's Dialog component traps focus inside the modal and returns it to the triggering element when the dialog closes. It applies aria-modal and handles Escape and Tab keys out of the box. You still need to load its CSS and understand that it renders into a portal attached to document.body.
How the focus trap works
A focus trap keeps the Tab key cycling inside the modal so keyboard users cannot accidentally leave the dialog. When the dialog closes, focus returns to the element that opened it. A portal renders the modal markup at the end of document.body, which helps it escape stacking contexts and clipping from parent containers. The aria-modal="true" attribute tells assistive technology that content behind the dialog is temporarily unavailable.
Worked example: install and render a dialog
Run the install command in your project's root directory. It does not require special permissions, but it writes to node_modules and updates package.json.
npm install @reach/dialog
Replace @reach/dialog with the exact version you intend to verify against your project's dependency policy. A missing CSS bundle is the most common reason the dialog appears invisible or unstyled.
import { Dialog, DialogContent } from '@reach/dialog';
import '@reach/dialog/dialog.css';
export function SettingsModal({ isOpen, onDismiss }) {
return (
<>
{ /* set isOpen true */ }}>Open settings
Settings
Change your preferences here.
Close
);
}
Open the dialog with the mouse or keyboard, press Tab, and confirm focus cannot leave the DialogContent region. Press Escape to close it. Open browser developer tools, run document.activeElement in the console, and confirm the active element is the Open settings button. In the Elements panel, look for role="dialog" and aria-modal="true" on the dialog container.
Risk: custom global key listeners that intercept Escape before Reach UI can handle them may break the close-and-return behavior. Scope those handlers to specific elements rather than attaching them to window.
Where this approach reaches its limit
Because Reach Dialog portals to document.body, it cannot live inside a constrained scrollable container such as a fixed sidebar with overflow: auto without additional positioning logic. If your design requires the modal to scroll within a parent element, you will need custom positioning or a different component. The component also assumes you want focus returned to the trigger; if your workflow requires focus to move elsewhere after close, you must handle that manually.
Practical check: render the dialog inside a scrollable container and open it. If the modal escapes the container or clips unexpectedly, the portal behavior is the cause.
Rollback
Installing the package changes your project's dependency tree. To revert, run the uninstall command in the same terminal:
npm uninstall @reach/dialog
Then remove the import and component code if you no longer need the modal.
Closing
Start with one modal in your application. Verify that focus returns to the trigger after close, then apply the same pattern to other dialogs. If you hit a constrained container, treat the portal behavior as a signal to reconsider the layout rather than patch around it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.