The body needs to stay exposed to assistive technology
- Rule:
axe/aria-hidden-body - Checks: aria-hidden=“true” must not be present on the document body
- WCAG: 1.3.1, 4.1.2
- Fix class:
mechanical: A fix needs no authored text or visual decision.
Who it affects
Section titled “Who it affects”- People who use a screen reader or a braille display, most of them blind.
How to fix it
Section titled “How to fix it”Script hides the page behind a modal dialog with aria-hidden="true" on <body>
Section titled “Script hides the page behind a modal dialog with aria-hidden="true" on <body>”Hide the dialog’s surroundings, not <body>: keep the dialog outside the page’s wrapper and set inert on the wrapper while the dialog is open. A native <dialog> opened with showModal() does this for you. On <body>, aria-hidden hides the whole page, the dialog included.
<body> <div class="page" inert> <main> <h1>Checkout</h1> </main> </div> <div role="dialog" aria-modal="true" aria-labelledby="signin-title"> <h2 id="signin-title">Sign in to continue</h2> <button type="button">Close</button> </div></body>The attribute stays after the overlay that set it has closed, or was copied in with a template
Section titled “The attribute stays after the overlay that set it has closed, or was copied in with a template”Remove it, and remove any hiding in the same code that closes the overlay, so the page is never left hidden.
Fixes that fail
Section titled “Fixes that fail”Setting aria-hidden="false" on <body>
Section titled “Setting aria-hidden="false" on <body>”axe passes, but ARIA cautions against "false" because browsers handle it inconsistently. Remove the attribute instead.
Moving aria-hidden="true" to a wrapper that also holds the dialog
Section titled “Moving aria-hidden="true" to a wrapper that also holds the dialog”The dialog is hidden along with the page, so screen reader users land in a dialog nothing announces. Hide only the dialog’s surroundings.
Hiding the surroundings with aria-hidden but leaving them focusable
Section titled “Hiding the surroundings with aria-hidden but leaving them focusable”Keyboard users can still tab out of the dialog into content screen readers will not announce. inert removes both focus and the content from the accessibility tree.
In React
Section titled “In React”The part of the fix that differs in React, in its syntax. The rest of the guidance holds.
A modal component sets aria-hidden on document.body from an effect
Section titled “A modal component sets aria-hidden on document.body from an effect”Render the dialog with createPortal into document.body, outside the element that wraps the rest of the app, and set inert={open} on that wrapper; React 19 writes inert as a boolean attribute. Or render a native <dialog> and call showModal() on it from an effect.
import { useId } from 'react';import { createPortal } from 'react-dom';
function SignInLayout({ dialogOpen, children }) { const titleId = useId(); return ( <> <div className="page" inert={dialogOpen}> {children} </div> {dialogOpen && createPortal( <div role="dialog" aria-modal="true" aria-labelledby={titleId}> <h2 id={titleId}>Sign in to continue</h2> <button type="button">Close</button> </div>, document.body )} </> );}Fixes that fail in React
Section titled “Fixes that fail in React”Setting aria-hidden on the app’s root element while the dialog renders inside it
Section titled “Setting aria-hidden on the app’s root element while the dialog renders inside it”The dialog is a descendant of the root, so it is hidden along with the page. Portal it outside the hidden element.
Setting the attribute in an effect with no cleanup
Section titled “Setting the attribute in an effect with no cleanup”If the dialog unmounts another way, such as on a route change, nothing removes the attribute and the page stays hidden.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.