Open a modal on your site, the cookie settings, a confirm step, or a sign-in box, and press Tab a few times. If the focus ring disappears behind the dark overlay and lands on a link in the page underneath, a keyboard user is now working in a part of the page they can't see.
Web HIG tip #13 (Quick #73 / HIG-A11Y-008): Modals must trap focus inside and restore focus to the trigger on close.
Why it matters
A modal says "deal with this first". Focus has to agree with what the screen shows:
- If Tab leaves the dialog, keyboard and switch users end up on controls hidden behind the overlay, and pressing Enter acts on something they can't see
- Screen reader users who move past the dialog's content read the page behind it as if the dialog weren't there
- If focus isn't moved in when the dialog opens, the user has to hunt for it, or doesn't know it opened at all
- If focus isn't put back on close, it usually drops to the top of the page, and the user has to find their place again
The full HIG covers this in §5.3, Keyboard Focus & Interaction. HIG-A11Y-008 (Modal focus containment, severity error) asks for three things: focus moves into the dialog when it opens, Tab and Shift+Tab cycle within it while it's active, and focus returns to the control that opened it when it closes. The spec is explicit that aria-modal="true" tells assistive technology the dialog is modal but does not itself create a focus trap. The keyboard pattern table adds that Escape closes a dialog. On the WCAG side, this supports 2.4.3 Focus Order (Level A). A trap with no way out would fail 2.1.2 No Keyboard Trap (Level A), so the Escape key and a visible close button are part of the rule, not extras.
The rule of thumb
Ask: while the dialog is open, can Tab ever reach something behind it? And when it closes, am I back where I started?
The native <dialog> element, opened with showModal(), does most of the work:
<button type="button" id="open-settings">Cookie settings</button>
<dialog id="settings" aria-labelledby="settings-title">
<h2 id="settings-title">Cookie settings</h2>
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="save">Save</button>
</form>
</dialog>
const dialog = document.getElementById('settings');
const trigger = document.getElementById('open-settings');
trigger.addEventListener('click', () => dialog.showModal());
showModal() makes the rest of the page inert, so Tab can't reach it, moves focus into the dialog, and closes it on Escape. Modern browsers also return focus to the element that was focused before the dialog opened. Test that in the browsers you support rather than assuming it. Tab can still move into the browser's own controls, such as the address bar. That's expected: the rule is about the page behind the dialog.
Do this instead
- Prefer
<dialog>withshowModal()over a custom<div role="dialog">. If you have to keep a custom modal, setinerton the page content behind it, move focus in yourself, and handle Escape - Don't rely on
aria-modal="true"alone. It changes what assistive technology is told, not where Tab goes - Pick the starting point on purpose: the first field in a form, the title for a long read, the safest button for a destructive confirm
- On close, return focus to the trigger. If the trigger no longer exists, for example the row was deleted, move focus to the next logical place, such as the list or the following row
- Give every dialog a visible close or cancel button as well as Escape
- Use
show()or a popover for non-modal UI. A dialog that doesn't block the page shouldn't trap focus either
Quick check for your app
Open a dialog on example.com using only the keyboard. Check that focus lands inside it. Press Tab and Shift+Tab ten times each: focus should stay in the dialog and never land on the page behind the overlay. Press Escape: the dialog should close and focus should be back on the button you opened it with. Repeat with a screen reader and with your confirm, sign-in, and cookie dialogs. A tool can tell you a dialog has aria-modal, but only tabbing through shows whether focus actually stays put.
The Web HIG is an open set of guidelines for how the web should behave, not a component library. Design systems define how it looks. The Web HIG defines how it behaves.
- GitHub: https://github.com/frozonfreak/webhig/discussions/29
- Hashnode: https://thewebhig.hashnode.dev/keep-focus-inside-the-dialog-web-hig-tip-13
- Dev.to: https://dev.to/frozonfreak/keep-focus-inside-the-dialog-web-hig-tip-13-44eo
- Bluesky: https://bsky.app/profile/sastha.bsky.social/post/3mz3oafs7zs2l
Top comments (1)
Worth testing the other ways out too. The close button usually gets checked, while Escape and a click on the overlay don't, and that's where focus tends to get lost. Native
<dialog>withshowModal()handles both parts of this tip: it makes the rest of the page inert and returns focus on close.