
The Focus Trap You Didn't Have to Write
Stop fighting with 'aria-hidden' and complex focus-trap-react wrappers—there is now a native way to tell the browser to ignore everything but your active modal.
You’ve spent more time debugging focus traps than you have building actual features, and frankly, it’s a waste of your life. For years, we’ve been told that to build a truly accessible modal, we need a 15KB library, a recursive function to find all focusable elements, and a delicate dance with aria-hidden on every single sibling of our modal’s parent container. It’s fragile, it’s heavy, and as of now, it’s completely unnecessary.
The browser finally gave us a way to just... stop. It’s called inert, and it’s the best thing to happen to HTML since we stopped using tables for layouts.
The Old Way Was a Nightmare
Before we dive into the solution, let’s pour one out for the code we used to write. To prevent a user from "tabbing out" of a modal and into the background page, we usually had to do something like this:
1. Find the modal.
2. Listen for the keydown event.
3. Check if the key is Tab.
4. Check if the user is on the last focusable element.
5. Manually wrap the focus back to the first element.
6. Remember to do the reverse for Shift + Tab.
It looks like a mess of querySelector calls and brittle logic. If you add a new button to your modal? Your trap might break. If you forget to remove the event listener? Memory leak.
Enter the inert Attribute
The inert attribute is a global HTML attribute that tells the browser to ignore a specific chunk of the DOM. When an element is marked as inert:
- The browser ignores all user input events (clicks, taps).
- The element and its children are removed from the tab order.
- The element and its children are hidden from the accessibility tree.
It essentially turns a piece of your website into a ghost. You can see it, but you can’t touch it, and screen readers act like it doesn't exist.
A Simple Implementation
If you have a modal open, you don't need a complex library to "trap" the focus. You just need to tell the rest of your app to stay quiet.
<body>
<!-- Everything that ISN'T the modal goes in a wrapper -->
<div id="main-content">
<h1>My Awesome App</h1>
<button onclick="openModal()">Open Modal</button>
<p>Lots of focusable links and content here...</p>
</div>
<!-- The Modal -->
<dialog id="my-modal">
<form method="dialog">
<h2>I'm a Modal</h2>
<button>Close Me</button>
</form>
</dialog>
</body>When you open that modal, you simply toggle inert on the #main-content:
const mainContent = document.getElementById('main-content');
const modal = document.getElementById('my-modal');
function openModal() {
// Make the rest of the page non-interactive
mainContent.setAttribute('inert', '');
modal.showModal();
}
function closeModal() {
// Bring the page back to life
mainContent.removeAttribute('inert');
modal.close();
}By making the background inert, you’ve solved the focus trap problem. The browser *literally cannot* move focus to anything inside #main-content because it’s no longer part of the interactive map.
Why This Beats aria-hidden
In the "before times," we used aria-hidden="true". While that helped screen readers, it did absolutely nothing for sighted keyboard users. You could still Tab into a "hidden" link, causing your focus ring to disappear into the void.
inert is the silver bullet because it handles the visual interaction, the keyboard interaction, and the assistive technology interaction all in one go. It’s a "real" browser state, not just a hint for a screen reader.
Practical Example: The Side Drawer
Let's say you have a side-nav that slides in. You don't want people accidentally clicking links in the background while the drawer is open.
const toggleDrawer = (isOpen) => {
const drawer = document.querySelector('.side-drawer');
const appBody = document.querySelector('.content-wrapper');
if (isOpen) {
drawer.classList.add('active');
appBody.setAttribute('inert', '');
} else {
drawer.classList.remove('active');
appBody.removeAttribute('inert');
}
};One line of JavaScript replaces an entire focus-trap-react dependency. Your bundle size will thank you.
The "Gotchas" (Because There's Always One)
Nothing is perfect. Here is what you need to keep in mind:
1. Browser Support: It’s actually great. As of 2023, inert is supported in all major evergreen browsers (Chrome 102+, Safari 15.5+, Firefox 112+). If you have to support ancient versions of IE, you’ll still need a polyfill.
2. Pointer Events: inert stops pointer events (clicks). If you still want the background to be clickable but just not "tab-able," inert is the wrong tool. But for modals and drawers? It’s exactly what you want.
3. The Root Element: Don't put inert on the <body> if your modal is *inside* the body. You’ll freeze your modal too. This is why it’s a good idea to have a main wrapper (like #root or #app) and keep your modals outside of it.
Stop Fighting the Browser
We have a tendency in web development to over-engineer things. We build complex JavaScript solutions for problems the browser is eventually going to solve for us.
If you are still manually calculating tab indices or using heavy wrappers to manage modal focus, it's time to delete that code. Use the <dialog> element where you can, and use inert for everything else. Your users get a better experience, and you get to maintain fewer lines of brittle logic. That's what I call a win-win.

