( Robust / WCAG 4.1.2 )
aria-hidden="true" is present on the document body
What is this issue?
The <body> element carries aria-hidden="true", an ARIA attribute that removes an element and every one of its descendants from the accessibility tree the browser builds for assistive technology. ARIA (Accessible Rich Internet Applications), a set of HTML attributes that describe the role, state, and properties of custom interface elements to assistive technology, includes aria-hidden specifically to hide content that’s visually present but shouldn’t be announced, like a decorative icon duplicating adjacent text.
Applied to body, that same mechanism hides everything on the page at once, since every element on the page is a descendant of body by definition. This is different from correctly using aria-hidden on a sibling of a modal dialog while it’s open, which is a valid and common pattern; the failure here is specifically the attribute landing on the root container that everything, including the dialog itself, lives inside.
Why does this matter?
aria-hidden="true" doesn’t just mute a section of content; it removes the entire subtree from the DOM (Document Object Model) as far as the accessibility tree is concerned, meaning assistive technology behaves as if that content, and everything inside it, doesn’t exist. When that subtree is body, there is no remaining content on the page for a screen reader to read, navigate, or interact with at all.
This bug is almost always accidental and almost always temporary in the code, but its effect while active is total. A modal library that sets aria-hidden="true" on background content to correctly hide it while a dialog is open, then fails to remove that attribute when the dialog closes (because the close button, the Escape key, and clicking outside the dialog each trigger a slightly different code path, and only some of them include the cleanup step), can leave a user’s entire page permanently hidden from assistive technology until they reload it.
Who is affected?
- Screen reader users: get a completely blank, silent page, since every element is removed from the accessibility tree at once; there is nothing to navigate to, read, or interact with until the attribute is removed.
- Voice control users: lose the ability to target anything on the page by name, since voice control software matches spoken commands against the same accessibility tree a screen reader reads from, which now contains nothing.
What users experience
Anders uses NVDA on Windows to check his account balance on a banking site. He opens a “Transfer funds” confirmation dialog, then presses Escape to cancel it after changing his mind. The modal library closes the dialog visually but only removes aria-hidden from the background wrapper on its “X” button click handler, not its Escape-key handler, a gap introduced when the library was extended without testing every close path. NVDA goes completely silent: no page content, no navigation landmarks, nothing. Anders assumes NVDA has crashed and restarts it, only for the same silence to return, before eventually reloading the entire page to recover access to his own account.
How do I fix it?
Never apply aria-hidden directly to body. This works simply because avoiding the mistake at its source prevents the entire class of failure: there’s no legitimate reason for body itself to be hidden from assistive technology, so any occurrence of it is a bug to fix, not a configuration to adjust.
If you’re hiding background content while a modal or dialog is open (a valid, common pattern), target a dedicated wrapper element that contains everything except the dialog, such as a <div id="app"> wrapping your main application content, siblings to the dialog itself. Whichever code manages opening the dialog should apply aria-hidden="true" to that wrapper, and, critically, every single code path that can close the dialog (a close button, the Escape key, clicking the overlay, a successful form submission) needs to remove it again, not just the primary “close” button handler.
Code Examples
<body aria-hidden="true">
<div id="app">...</div>
<div class="modal" role="dialog" aria-modal="true">...</div>
</body><!-- aria-hidden targets a wrapper around the app, not body itself -->
<body>
<div id="app" aria-hidden="true">...</div>
<div class="modal" role="dialog" aria-modal="true">...</div>
</body>Moving aria-hidden="true" from body to the #app wrapper preserves the intended effect (background content is hidden from assistive technology while the dialog is open) without also hiding the dialog itself, since the dialog now sits as a sibling outside the hidden wrapper rather than nested inside it. The modal’s own role="dialog" and aria-modal="true" remain untouched and fully functional once they’re no longer trapped inside a hidden ancestor.
Common Mistakes
Mistake: “I’ll set aria-hidden on body because it’s the simplest way to hide everything except the modal.” Simplicity here comes at the cost of hiding the modal too, since it’s also a descendant of body. The correct simple pattern is hiding a wrapper around everything except the dialog, which requires the dialog to be a DOM sibling of that wrapper, not nested inside it.
Mistake: “I only need to clean up aria-hidden on the button that normally closes the modal.” Users close dialogs through several different interactions (a close button, the Escape key, clicking the overlay, a successful form submission), and each one needs its own cleanup step if it uses a separate code path. Covering only the most obvious close button leaves every other exit route capable of stranding the page hidden.
Mistake: “This bug only affects screen reader users, so it’s lower priority than a visual bug.” A bug that hides an entire page from a specific group of users but leaves it visually normal for everyone else is easy to deprioritize precisely because it’s invisible to the majority of a QA team’s manual testing. The severity here is total for the affected users, even though the visual bug tracker never sees a symptom.
How RedFlag Detects This
Automated: axe-core rule, runs on every scan. RedFlag calls axe-core’s aria-hidden-body rule as part of every scan, restricted to the WCAG 2.0/2.1/2.2 A and AA rule set. The rule performs a single, direct check: whether the <body> element itself carries aria-hidden="true" at the moment the page is scanned.
False negative: RedFlag’s scan captures a snapshot of the page at scan time; a modal-cleanup bug that only leaves body hidden after a specific user interaction, like pressing Escape rather than clicking a close button, won’t be caught unless that exact interaction happens to occur during or before the scan. False positive: none typical for this check, since detecting a single attribute’s presence on a single, unambiguous element is a binary condition axe-core evaluates reliably. Manual step: open every modal, drawer, or overlay on the page and close it through every available method (close button, Escape key, overlay click, successful submission), checking after each one that body never ends up with aria-hidden="true" left behind.
Manual Testing
- Open the page in Chrome or Firefox with NVDA or VoiceOver running.
- Open every modal, dialog, or overlay component on the page one at a time.
- Close each one using every method available: the close button, the Escape key, clicking outside the dialog, and successfully submitting any form inside it.
- After each close, confirm the screen reader can still read and navigate the rest of the page normally. If it goes completely silent or reports an empty page,
aria-hidden="true"has likely been left onbody. - Open the browser’s accessibility inspector (Chrome DevTools → Elements → Accessibility pane), select the
<body>element, and confirm it does not showaria-hidden: truein its computed accessibility properties.
Related WCAG Success Criteria
4.1.2 Name, Role, Value: Every interface component must expose a name, role, and value to assistive technology. aria-hidden on body is the most total possible failure of this criterion, since it removes every component on the page from that exposure simultaneously, not just one.
Related Issues
Focusable element is inside an aria-hidden container covers the closely related failure of the same aria-hidden mechanism applied more narrowly: a container correctly scoped away from body, but still containing a focusable element it shouldn’t.
ARIA attribute has an invalid value and ARIA attribute name is not valid cover the same aria-hidden attribute’s spelling and value-format correctness: a page can pass those checks and still fail this one if the value is valid but applied to the wrong element.
ARIA attribute is not valid for the current role value shares the same “correctly-specified ARIA in the wrong structural context” theme, applied to conditional attribute-role pairings rather than an attribute on the wrong ancestor.
Required ARIA attribute is missing is worth checking on the modal dialog itself once the body-hiding bug is fixed, since a dialog’s own required attributes are a common adjacent gap in the same component.
References
- W3C Understanding 4.1.2: Name, Role, Value
- MDN: aria-hidden
- ARIA Authoring Practices: Dialog (Modal) Pattern
Frequently asked questions
Can aria-hidden ever be used correctly somewhere near the body element?
Yes. Modal dialogs correctly use aria-hidden on a wrapper element around the rest of the page content, siblings of the dialog, while the page is open: the technique is valid and common. The failure this rule catches is specifically the attribute landing on body itself, which hides the dialog too, along with everything else.
Does aria-hidden="true" on body also stop keyboard navigation?
No, not directly. aria-hidden only affects the accessibility tree that screen readers and similar assistive technology read from; it does not remove elements from the keyboard tab order on its own. A sighted keyboard user can often still tab through the page even while a screen reader user is completely locked out.
Which JavaScript libraries commonly cause this bug?
Most commonly, custom or older modal, drawer, and dialog implementations that manually toggle aria-hidden on background content using a selector that unintentionally matches body, or that fail to remove the attribute in every code path a dialog can close through (an Escape key press, an overlay click, and a close button each need their own cleanup).
Why is this rated critical impact instead of serious or moderate?
Because the blast radius is the entire page, not one component. Every other ARIA and accessibility issue on a page becomes irrelevant to a screen reader user once body itself is hidden, since none of that content is reachable at all; this single attribute can undo the effect of every other accessibility fix on the page simultaneously.
Does this ever happen outside of modal dialogs?
Occasionally, yes. Some third-party analytics, cookie-consent, or chat-widget scripts manipulate aria-hidden on page regions for their own overlay behavior, and a bug in that third-party script's cleanup logic can leave body hidden even when no modal-like component built by your own team is involved.