Focus trapping
the inert attribute
Chrome102
Edge102
Firefox112
Safari15.5
Features it needs
- inertWidely available
A focus trap works by listening for keydown, noticing that Tab has left the overlay, and putting focus back. The inert attribute comes at it from the other side: mark the rest of the page inert and the browser drops it from the tab order, from the accessibility tree and from pointer events, so there is no escape to catch. If the overlay is a <dialog> opened with showModal(), the browser applies the same treatment to everything behind it and you write nothing at all.
When this applies
Keeping keyboard focus inside an open modal, drawer or menu.
The native approach
<!-- While the panel is open, the rest of the page is unreachable:
no tab stops, no clicks, and hidden from screen readers. -->
<main id="page" inert>...</main>
<aside class="panel">...</aside>When the dependency is still right
An answer that always says "the platform covers it" is worse than no answer. These are the cases where this one does not hold.
- Focus has to cycle from the last element in the overlay back to the first. inert stops focus reaching the rest of the page, and the browser then moves on to the URL bar rather than wrapping inside your container.
- You rely on the library restoring focus to the element that opened the overlay. inert does not track that, so storing the previous activeElement and calling focus() on close is yours to write.
- The overlay lives inside the same subtree you would have to mark inert, so there is no ancestor you can set the attribute on without disabling the overlay along with it.
- You are using the initial-focus, Escape handling and scroll locking the library bundles around the trap, and separating those out costs more than keeping the dependency.
- Your support target includes browsers older than Chrome 102, Firefox 112 or Safari 15.5. They ignore inert, and focus escapes quietly rather than failing in a way you would notice.
Building it
This catalog stops at the swap. These guides go through the implementation and the fallbacks, and each one reads in the terminal through its own command.
- Accessibility
npx -y modern-web-guidance@latest retrieve "accessibility" - Navigation drawer
npx -y modern-web-guidance@latest retrieve "navigation-drawer"
Signs it was hand-rolled
No package is involved in any of these, so nothing would match in a package.json. If the code looks like one of them, this rule applies anyway, and the conditions above still decide.
- a keydown handler watching for Tab and calling preventDefault so focus cannot leave an overlay
- a querySelectorAll over a list of focusable selectors, used to decide where focus is allowed to go