Visitar URL original
USWDS - Bug: Modal and header leave the page aria-hidden after teardown · Issue #6830 · uswds/uswds · GitHub
Skip to content

USWDS - Bug: Modal and header leave the page aria-hidden after teardown #6830

Description

@vssinghh

Describe the bug

When a modal or the mobile navigation opens, USWDS sets aria-hidden="true" on all other body content. This is correct. It stops screen readers from reading the page behind the open component.

The close path removes those attributes. The teardown path does not. If off() runs while the component is still open, the attributes stay. The whole page remains hidden from assistive technology, and there is no recovery except a page reload.

This affects two components:

  • usa-header: teardown() removes the resize listener and resets navActive. It does not call showNonNavItems().
  • usa-modal: teardown() calls cleanUpModal(), which restores the modal to its original place in the DOM. It does not remove aria-hidden or data-modal-hidden from non-modal elements.

For a screen reader user, the page reads as empty. This is a major functionality problem for that user, and it is silent for everyone else.

Workaround: close the component before calling off().

Steps to reproduce the bug

Save this as repro-short.js in the repo root and run node repro-short.js.

// Run from the repo root: node repro-short.js
const { JSDOM } = require("jsdom");

const dom = new JSDOM(`<!DOCTYPE html><body>
  <div class="usa-overlay"></div>
  <header class="usa-header">
    <div class="usa-nav-container">
      <button type="button" class="usa-menu-btn">Menu</button>
      <nav class="usa-nav">
        <button type="button" class="usa-nav__close">Close</button>
        <ul class="usa-nav__primary"><li><a href="#one">One</a></li></ul>
      </nav>
    </div>
  </header>
  <main id="main-content"><h1>Page content</h1></main>
</body>`, { pretendToBeVisual: true });

for (const k of ["window", "document", "navigator", "HTMLElement", "Element", "Event", "getComputedStyle"]) {
  global[k] = k === "window" ? dom.window : dom.window[k];
}

const navigation = require("./packages/usa-header/src/index.js");
const main = document.getElementById("main-content");
const show = (l) => console.log(`${l.padEnd(22)} aria-hidden=${main.getAttribute("aria-hidden")}`);

navigation.on(document.body);

navigation.toggleNav.call(navigation, true);
navigation.toggleNav.call(navigation, false);
show("closed normally");        // control: restored

navigation.toggleNav.call(navigation, true);
navigation.off(document.body);
show("off() while open");       // bug: still hidden

Output:

closed normally        aria-hidden=null
off() while open       aria-hidden=true

The first line is the control. A normal close restores the page. The second line is the bug.

I also tested off() while the nav is already closed. That is harmless. So the problem is specific to teardown while the component is open.

The same test on usa-modal gives the same result. Open the modal, call modal.off(document.body), and non-modal content keeps aria-hidden="true".

Expected Behavior

teardown() should restore the page content that the component hid from assistive technology. Page content must never be left hidden after a component is removed.

usa-modal already does this correctly on close. The restore loop runs unconditionally, because the opener may be gone from the document by then. Teardown needs the same guarantee.

Only the assistive technology state needs restoring. packages/usa-nav/src/test/navigation.spec.js documents off() as removing event listeners without closing an open component, so visible state should be left alone.

Related code

packages/usa-header/src/index.js:258. teardown() never calls showNonNavItems() (line 63).

packages/usa-modal/src/index.js:392. teardown() calls cleanUpModal(), which restores the modal's original DOM location but never removes aria-hidden or data-modal-hidden. The working restore loop is at line 180, in the close path only.

Additional context

Calling off() in a component's destroy lifecycle is the normal pattern for framework users, and #5062 describes teams doing this in React, Vue, Angular, and Nuxt. An unmount or route change while a modal or the mobile menu is open triggers this.

Related to #6785 and PR #6786, which fixed the same class of problem on the modal close path. That fix does not cover teardown.

I have a fix for both components with regression tests and will open a PR shortly.

Activity

  1. added
    Affects: Accessibility 🟡Affects access for disabled users or assistive tech. Record the barrier; not a severity score.
    Needs: ConfirmationReported behavior needs validation. Remove after recording reproduction or investigation results.
    Package: ModalWork affecting Modal. Apply only when this area helps route or find the issue.
    on Aug 20, 2026
  2. added theissue type on Sep 16, 2026
  3. added
    Package: HeaderWork affecting Header. Apply only when this area helps route or find the issue.
    and removed
    Needs: ConfirmationReported behavior needs validation. Remove after recording reproduction or investigation results.
    on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Affects: Accessibility 🟡Affects access for disabled users or assistive tech. Record the barrier; not a severity score.Context: JavaScriptIssue is in JavaScriptPackage: HeaderWork affecting Header. Apply only when this area helps route or find the issue.Package: ModalWork affecting Modal. Apply only when this area helps route or find the issue.

    Type

    Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions