Who it affects
Someone who is blind, searching a university library catalogue opens a filter dialog, closes it, and the whole catalogue falls silent. Tab moves through links that are never announced.
Someone using a screen reader on a page where a cookie banner set the attribute gets a blank page from the start and assumes the site is down.
Someone using voice control, which reads the same accessibility tree has no targets to name, so cannot click anything.
Broken versus fixed
The broken markup is shown as code rather than rendered, because the effect is in how it is announced or handled, not how it looks.
<body aria-hidden="true">
<header>...</header>
<main>
<h1>Library catalogue</h1>
<form>...search fields...</form>
</main>
</body>
The whole document is removed from the accessibility tree. Focus still moves through the page, but nothing is announced.
<body>
<header>...</header>
<main>
<h1>Library catalogue</h1>
<form>...search fields...</form>
</main>
<dialog aria-labelledby="filters-title">...</dialog>
</body>
The page is exposed. A modal uses the dialog element and showModal(), or aria-modal="true" on a custom dialog, and never touches the body.
Test it by hand in two minutes
- Right click anywhere, choose Inspect, and look at the body tag at the top of the Elements panel for aria-hidden="true".
- Open and close every dialog, menu and cookie banner on the page, then look at the body again. Scripts often set the attribute on open and fail to remove it on close, on Escape or on a click outside.
- With a screen reader running, Tab through the page. If focus moves but nothing is announced, something above the content is hidden.
- Search the JavaScript for setAttribute('aria-hidden' and check that every path that sets it also removes it.
The fix
<body aria-hidden="true">
<body>
Remove the attribute. Then find the script that sets it and make it target the page wrapper or the dialog's siblings, and always remove it when the dialog closes.
Dialog script that hides the page
document.body.setAttribute('aria-hidden', 'true');
const dialog = document.getElementById('filters');
dialog.showModal();
The native dialog element with showModal() makes the rest of the page inert for you, and undoes it on close. No aria-hidden needed.
Custom dialog with inert on the page wrapper
<body aria-hidden="true">
<div id="page">...</div>
<div role="dialog">...</div>
</body>
<body>
<div id="page" inert>...</div>
<div role="dialog" aria-modal="true" aria-labelledby="filters-title">...</div>
</body>
inert hides the wrapper from assistive technology and from the keyboard, which is what a modal needs. Remove it when the dialog closes.
Common mistakes
- Setting aria-hidden on body to hide the page behind a dialog, then removing it only from the Close button and not on Escape or a backdrop click.
- A cookie banner script that sets aria-hidden on html or body and never runs its cleanup when consent is already stored.
- Putting aria-hidden on a wrapper that also contains the dialog, which hides the dialog too.
- Adding aria-hidden to body to silence a noisy third-party widget.
WCAG criteria this rule maps to
axe-core tags this rule as WCAG 2.2 level A. It is one of the checks for:
- 1.3.1 Info and Relationships (level A). Official W3C text.
- 4.1.2 Name, Role, Value (level A). Official W3C text.
The scanner can see aria-hidden="true" on the body element at the moment it runs. If a script adds it only while a dialog is open and removes it correctly, the scan will not see it, so test the open and close paths by hand. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.
Related rules
aria-hidden-focus
aria-hidden="true" tells screen readers to ignore an element, but it does not stop the Tab key reaching the links and buttons inside.
A
Frames with controls inside must not have tabindex=-1
frame-focusable-content
An iframe with tabindex="-1" is taken out of the Tab order, and so is everything inside it.
A
Give people a way to skip past repeated navigation
bypass
Most pages start with the same header and menu.