HTML · · 6 min read

That One HTML Attribute I Wish I Knew About Sooner

The inert attribute solves focus trapping without JavaScript ceremony. A 40-line focus trap function replaced by four lines that actually work.

I was debugging a focus issue in a USWDS mobile nav and stumbled into something that made me feel both relieved and a little embarrassed. The inert attribute has been sitting in the HTML spec for years, quietly solving a problem I've probably been over-engineering since the jQuery days.

The Bug

Here's the setup. I'm working on a VA project using the USWDS mobile navigation pattern. The off-canvas menu slides in, an overlay covers the page, and everything looks right. Visually, it's fine. But tab through the page with a keyboard and you'll find the problem fast: focus bleeds right through the overlay and into the content behind it.

Links in the footer. Form fields buried halfway down the page. The skip nav link. All reachable. All wrong.

This is a 508 compliance issue, and on a federal project, that's not a "we'll get to it" kind of bug. That's a "stop what you're doing and fix it" kind of bug.

The Old Way

If you've been doing this long enough, you know the playbook. You write a focus trap. You grab every focusable element inside the nav, figure out the first and last one, then intercept Tab and Shift+Tab to keep the user cycling within the drawer. Something like this:

const focusableSelectors = [
  'a[href]',
  'button:not([disabled])',
  'input:not([disabled])',
  'select:not([disabled])',
  'textarea:not([disabled])',
  '[tabindex]:not([tabindex="-1"])',
];

function trapFocus(container) {
  const focusableElements = container.querySelectorAll(
    focusableSelectors.join(', ')
  );
  const firstFocusable = focusableElements[0];
  const lastFocusable = focusableElements[focusableElements.length - 1];

  container.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey) {
      if (document.activeElement === firstFocusable) {
        e.preventDefault();
        lastFocusable.focus();
      }
    } else {
      if (document.activeElement === lastFocusable) {
        e.preventDefault();
        firstFocusable.focus();
      }
    }
  });
}

It works. It also sucks. You have to maintain that selector list. You have to handle dynamically added elements. You have to remember to clean up the event listener when the menu closes. You have to sprinkle aria-hidden="true" on the main content so screen readers don't wander off. And you still end up with edge cases where something sneaks through.

I've written some version of this function on a variety of projects over the years. It always felt like too much ceremony for what should be a simple concept: "hey browser, ignore everything else when this is open."

Enter inert

The inert attribute does exactly that. It's a boolean attribute you put on any element, and it tells the browser to make that entire subtree non-interactive. No clicks. No focus. No keyboard navigation. Screen readers skip it entirely.

<body>
  <header>...</header>
  <nav class="usa-nav" aria-expanded="true">
    <!-- Mobile nav content -->
  </nav>
  <main id="main-content" inert>
    <!-- All of this is now unreachable -->
  </main>
  <footer inert>
    <!-- This too -->
  </footer>
</body>

That's it. When the mobile nav opens, add inert to the elements behind it. When it closes, remove it. No focus trap function. No selector lists. No event listener cleanup.

const mainContent = document.querySelector('#main-content');
const footer = document.querySelector('footer');

function openNav() {
  mainContent.setAttribute('inert', '');
  footer.setAttribute('inert', '');
}

function closeNav() {
  mainContent.removeAttribute('inert');
  footer.removeAttribute('inert');
}

I replaced about 40 lines of focus-trapping JavaScript with four lines that are easier to read and harder to break.

What inert Actually Does

It's worth understanding what's happening under the hood, because inert is doing more than just blocking Tab key navigation.

When you set inert on an element, the browser treats the entire subtree as if it doesn't exist from an interaction standpoint. That means:

  • No keyboard focus. Tab skips every focusable element inside.
  • No click events. Buttons and links don't respond.
  • No text selection. Users can't highlight or copy content.
  • Assistive tech ignores it. Screen readers won't announce any of it.

It's like combining tabindex="-1" on every focusable descendant, aria-hidden="true" on the container, and pointer-events: none in CSS, all in a single attribute. Except it actually works correctly and consistently, which is more than I can say for trying to wire all of those up by hand.

inert vs. disabled

This tripped me up at first. I'd been reaching for disabled on form controls for years, and inert felt like it overlapped. It doesn't.

disabled is a form control concept. It works on <input>, <button>, <select>, <textarea>, and <fieldset>. It prevents interaction, excludes the value from form submission, and the browser gives you that grayed-out look for free. Semantically, it means "this control exists but isn't available right now."

inert is a DOM-level concept. It works on any element. It doesn't change visual appearance at all (you have to handle that yourself). It doesn't affect form submission semantics. Its meaning is closer to "pretend this entire subtree isn't here."

The visual part is important. If you make something inert without any visual indication, your users will be confused when they can't interact with it. I added a simple style:

[inert] {
  opacity: 0.5;
  pointer-events: none;
}

The pointer-events: none is technically redundant since inert already blocks clicks, but I kept it as a belt-and-suspenders thing. Old habits.

For the mobile nav specifically, the overlay already handles the visual cue. The content behind it is dimmed and covered. So inert is really just making the browser's understanding match what the user already sees.

What inert Is Not

A few things worth calling out so nobody gets the wrong idea.

It's not a security mechanism. A user can open DevTools, remove the attribute, and interact with everything. Don't use it to protect form fields or hide sensitive data.

It's not a replacement for aria-hidden in every case. If you need to hide something from assistive tech but keep it interactive (rare, but it happens), aria-hidden is the right tool. inert always blocks both.

It's not a visibility tool. Inert content is still rendered and visible. If you want to hide something visually, you still need display: none or the hidden attribute.

The <dialog> Connection

If you're using the native <dialog> element with showModal(), the browser automatically makes the rest of the page inert. You get this behavior for free. That's one of the strongest arguments for using native <dialog> instead of rolling your own modal pattern.

The USWDS modal component may or may not be using native <dialog> under the hood depending on your version, but for the mobile nav, there's no dialog involved. It's a drawer. So you're on your own, and inert is the cleanest solution I've found.

Gotchas

A few things that caught me or are worth knowing.

Inheritance is absolute. If a parent is inert, every descendant is inert. There's no inert="false" on a child to opt it back in. Plan your DOM structure accordingly.

No CSS pseudo-class. Unlike :disabled, there's no :inert pseudo-class. You have to use the attribute selector [inert] for styling. Minor annoyance.

Browser support is solid now. Chrome 102+, Firefox 112+, Safari 15.5+, Edge. All the evergreen browsers have it. Unless you're supporting IE11 (my condolences), you're fine. There's a wicg-inert polyfill if you really need it, but at this point I'd question whether it's worth the weight.

The Takeaway

I've been building websites since 2000. I've written focus traps in jQuery, in vanilla JS, in Vue, in React. Every time, it felt like the kind of thing the platform should just handle. Turns out, the platform does handle it now. I just wasn't paying attention.

The inert attribute is one of those features that doesn't get a lot of fanfare. No one's writing Twitter threads about it. But for anyone doing accessibility work on government projects, or really any project where keyboard navigation and screen reader behavior matter, it's a genuine quality-of-life improvement.

Next time you're wiring up a focus trap by hand, stop. There's probably an inert attribute that wants to meet you.