Mina was running a keyboard-only pass on a settings page — no mouse, just Tab and Enter, the way a chunk of her users actually navigate. She opened the "Delete account" confirmation modal, pressed Tab a few times to reach Cancel, and kept going out of habit.
Tab landed on a button. It said "Delete account." The one behind the modal, on the page that was supposedly locked out while the confirmation dialog was up.
She pressed Enter. It fired.
The wrong-way checklist that still leaks
The modal wasn't half-built. Someone had clearly thought about this — the background had aria-hidden="true" slapped on it the moment the modal opened, which is the standard first move. Screen readers skipped right over it, and a quick VoiceOver pass looked clean.
Here's the problem: aria-hidden talks to exactly one audience — assistive tech. It's a signal in the accessibility tree, nothing more. It does not disable a click handler. It does not remove an element from focus. A sighted keyboard user tabbing through the page never consults the accessibility tree at all, so aria-hidden="true" was invisible to the one bug that mattered.
The next line of defense, if a team knows to reach for it, is walking every focusable descendant and setting tabindex="-1" — buttons, links, inputs, all of it — then walking them all again to undo it when the modal closes. That's real work, and it's also incomplete: tabindex="-1" only removes an element from the sequential Tab-key order. It does nothing to stop a direct element.focus() call from application code, and it does nothing to stop a plain click. A background button set to tabindex="-1" is still clickable. Try it — it's not a trick, that's just what the attribute is scoped to.
So the "complete" old-way checklist looks like: aria-hidden on the container, tabindex="-1" on every descendant, pointer-events: none in CSS so clicks stop registering, and a mental note to reverse exactly three of those things on close. Four separate mechanisms, none of which individually does the whole job, all of which have to stay in sync by hand.
One attribute, not four
<div id="background" inert>
<!-- everything in here: not focusable, not clickable, not selectable,
not in the accessibility tree, not matched by find-in-page -->
</div>
That's the entire fix. inert is a boolean global HTML attribute, and setting it on a container does all four jobs the old checklist did separately:
-
Focus is blocked, by any method. Not just Tab order — a direct
.focus()call on an inert element, or on any of its descendants, is a no-op per spec. Focus stays exactly where it was. This is the detailtabindex="-1"never covered. - Clicks don't fire. No click event reaches an inert element or anything inside it.
-
It's pulled from the accessibility tree, the same effect
aria-hiddengives you — so you get that half for free, correctly, without adding it yourself. - Text can't be selected, and browser find-in-page skips it entirely. Ctrl+F won't match a word buried in inert content.
And it's inherited down the whole subtree from one attribute on the container — you're not walking descendants, and there's nothing to carefully undo besides removing the one attribute you added.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
The part that surprises people: it doesn't look like anything
Set inert on a panel and reload — visually, nothing changes. Full opacity, full color, sitting right where it was. inert is purely behavioral; it was never meant to also be your dimming mechanism. That's a separate job, and CSS ships a selector built for exactly this handoff:
#background:inert {
opacity: 0.4;
filter: grayscale(0.6);
}
The :inert pseudo-class matches any element that's currently inert — whether it got that way from the attribute directly or from an inert ancestor — so the greyed-out look updates itself the instant the attribute is added or removed. No class to toggle in JS, no state to fall out of sync with the real behavior underneath it.
The gap that's easy to miss even once you know about inert
Here's the genuinely sharp edge: inert and tabindex="-1" both remove an element from the Tab key's path, so from a quick glance they look interchangeable. They aren't. tabindex="-1" only opts an element out of sequential navigation — the thing your keyboard walks. It was never designed to stop a script from calling .focus() on that same element, and plenty of code does exactly that (restoring focus after a modal closes, autofocusing a validation error, a testing library's userEvent.click which focuses before clicking). Point that kind of call at a tabindex="-1" element and it works fine. Point it at an inert one, and the spec's focusing steps refuse — the call is a no-op, full stop.
That's the difference between "the keyboard won't wander here" and "nothing reaches here." Mina's bug was the first kind pretending to be the second.
Where it doesn't help
inert isn't a permissions system — it's a blunt, whole-subtree switch. You can't make one descendant interactive again while its ancestor stays inert; if you need fine-grained control over which of ten buttons in a panel are usable, you're back to disabled on individual controls. And it's still your job to move focus somewhere sensible when you add or remove it — setting inert on the background doesn't focus your modal for you.
It's also worth checking your baseline before reaching for it without a second thought: inert has been supported across Chrome, Edge, Firefox, and Safari since 2023, so by 2026 it's safe to use directly in almost any modern app without a polyfill.
The fix, one line
<!-- open -->
<div id="background" inert>…</div>
<button id="modalConfirm">Delete</button>
<!-- close -->
<div id="background">…</div>
No descendant walk, no pointer-events rule to remember, no aria-hidden that only solved a quarter of the problem. One attribute, added when the modal opens and removed when it closes, and the background is actually — not just visually — out of reach.
Mina's team shipped that one-line fix the same afternoon. What's your team currently using to "disable" a background panel — and have you actually tried tabbing behind your own modal to check?
🧠 Test yourself
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Top comments (0)