( Perceivable / WCAG 1.3.1 )
Hidden component exposed to screen readers
What is this issue?
A component that should only be reachable while open (a modal, a dropdown menu, a slide-out panel) is visually hidden through CSS alone: opacity: 0, pointer-events: none, an off-screen transform, or a scale(0). None of those properties remove the element from the DOM (the tree structure the browser builds from your HTML, representing every element and how they nest), and none of them add aria-hidden or inert, so the accessibility tree still contains the component and everything inside it exactly as if it were open.
Assistive technology reads the DOM, not the rendered visual layout, so a component that looks completely gone to a sighted user can still be fully navigable to a screen reader user, buttons and all.
Why does this matter?
Screen reader users typically navigate a page by content or by element type (headings, links, form controls), not strictly by what’s currently visible on screen. If a closed modal’s buttons and text are still sitting in the accessibility tree, a screen reader user browsing that way can land directly inside content that a sighted user would swear isn’t there at all, with no visual cue explaining what they’ve just encountered.
This gets worse the more interactive the hidden component is: a closed modal with a “Delete account” button that’s still focusable and announced means a screen reader user tabbing through the page can land on and potentially activate a destructive action tied to a dialog they never opened, and have no visual context confirming what they’re about to do.
Who is affected?
- Screen reader users: encounter buttons, links, and text from closed components while reading or tabbing through the page, with no visual explanation for content that a sighted user can’t see at all.
- Keyboard users: can tab directly into a visually hidden component’s focusable elements, since properties like
opacity: 0and off-screen positioning don’t remove an element from the tab order. - Cognitive disabilities: encountering unexplained, seemingly out-of-place content interrupts the mental model a user has built of the page, adding confusion about what state the page is actually in.
What users experience
Leila uses VoiceOver on her Mac to browse a shopping site. She’s navigating by heading and stumbles into a “Confirm your order” dialog’s contents (a total, a “Place order” button, a “Cancel” button), even though she never opened a checkout modal and the page visually shows only the product listing. The modal was left in the DOM with opacity: 0 from a previous page state instead of being hidden from the accessibility tree. She has no way to tell whether this is a real, active checkout she’s about to trigger or leftover content she should ignore, and backs away from the whole section rather than risk placing an order she didn’t intend.
How do I fix it?
Add aria-hidden="true" and inert to the component’s container the moment it closes, and remove both attributes the moment it opens. This works because aria-hidden="true" removes the element and its descendants from the accessibility tree entirely, so screen readers skip past it as if it weren’t there, while inert additionally removes every descendant from the keyboard tab order and blocks pointer interaction; the two together match what a sighted user already sees: nothing.
Never rely on opacity, transform, or off-screen positioning alone to hide an interactive component. Those properties only affect the rendered visual layout; none of them touch the accessibility tree or the tab order on their own, which is exactly the gap this rule exists to catch.
Code Examples
<div id="checkout-modal" style="opacity:0;pointer-events:none">
<h2>Confirm your order</h2>
<p>Total: $84.00</p>
<button>Place order</button>
<button>Cancel</button>
</div><!-- Closed state: hidden from the accessibility tree and tab order -->
<div id="checkout-modal" aria-hidden="true" inert>
<h2>Confirm your order</h2>
<p>Total: $84.00</p>
<button>Place order</button>
<button>Cancel</button>
</div>// Open state: remove both attributes and move focus in
function openModal() {
const modal = document.getElementById('checkout-modal');
modal.removeAttribute('aria-hidden');
modal.removeAttribute('inert');
modal.querySelector('h2').focus();
}aria-hidden="true" is what stops a screen reader from ever announcing the modal’s contents while it’s closed, and inert is what stops a keyboard user from tabbing into it; CSS visibility alone never touched either of those mechanisms. Toggling both attributes in the same function that opens and closes the modal keeps the accessibility-tree state and the visual state from ever drifting apart.
Common Mistakes
Mistake: “opacity: 0 hides the element, so it’s already inaccessible.” opacity: 0 only changes what’s rendered on screen. It leaves the element fully present in the accessibility tree and the tab order: a screen reader can still navigate into it and a keyboard user can still tab into it, exactly as if it were fully visible.
Mistake: “aria-hidden alone is enough, since screen readers won’t announce it anymore.” aria-hidden="true" stops announcement, but it doesn’t remove focusable descendants from the keyboard tab order by itself in every browser. Pair it with inert (or manually strip tabindex from every focusable child) so keyboard users can’t tab into a component that’s supposedly hidden.
Mistake: “This is only a problem for modals.” Any component that’s conditionally shown and hidden (dropdown menus, slide-out panels, tooltips, off-canvas navigation) has the same risk whenever it’s hidden with CSS alone instead of aria-hidden/inert. The component type doesn’t matter; what matters is whether the hidden state is expressed to the accessibility tree, not just to the renderer.
Mistake: “Removing the element from the DOM entirely is always safer than aria-hidden/inert.” It’s a valid alternative for components that don’t need to preserve state while closed, but it isn’t strictly required; aria-hidden plus inert is equally correct and often simpler for a component with animation, transition state, or DOM measurements that need to persist between closed and open.
How RedFlag Detects This
Custom automated: RedFlag’s own DOM detector, runs on every scan. RedFlag runs two passes over the page. The first selects elements with role="dialog", role="alertdialog", role="menu", role="listbox", role="tooltip", or an aria-modal attribute. The second selects div, section, aside, and nav elements whose id or class name matches a pattern like “modal,” “overlay,” “drawer,” “panel,” “flyout,” “offcanvas,” “sidebar,” “popup,” or “lightbox.” Both passes flag elements that are not aria-hidden and not inert, but whose computed style or geometry (display, visibility, an off-screen bounding rect, opacity: 0, or a scale(0) transform) indicates the element is actually invisible.
False negative: a component hidden with a technique the detector doesn’t check for computed geometry against (for example, a clip-path that visually crops an element to nothing without changing its bounding rect or opacity) can pass undetected. False positive: the class/id pattern match in the second pass is broad; a genuinely visible component whose class name happens to contain “panel” or “popup” for unrelated naming reasons, while temporarily at low opacity mid-animation at the moment of the scan, can be flagged. Manual step: confirm the element is meant to be fully closed (not mid-transition) and that a screen reader genuinely cannot navigate into it while it’s in that state. Note: this same finding is also raised by RedFlag’s separate AI-assisted label-review flow under the related rule id hidden-accessible; the custom DOM detector documented here is the primary, authoritative source for this page’s automated classification.
Manual Testing
- Open the page in Chrome or Firefox with NVDA or VoiceOver running.
- Close every modal, dropdown, and slide-out panel on the page so it’s visually hidden.
- Use the screen reader’s element list (NVDA/JAWS: Insert+F7; VoiceOver: the Rotor) or navigate by heading/button to check whether any closed component’s content is still announced.
- Separately, Tab through the page with the keyboard alone and confirm Tab never lands inside a visually closed component.
- If a screen reader announces content from a closed component, or Tab reaches a focusable element inside one, the check fails.
Related WCAG Success Criteria
1.3.1 Info and Relationships: Information, structure, and relationships conveyed through presentation must also be available programmatically, and vice versa: what’s absent visually should be absent programmatically too. A component hidden only visually but still present in the accessibility tree is exactly this mismatch, which is why this rule maps to 1.3.1.
4.1.2 Name, Role, Value: Every interface component’s state, including whether it’s currently available for interaction, must be exposed accurately. A closed component that’s still focusable and announced misrepresents its own availability to assistive technology.
Related Issues
aria-hidden is used on the body element and Focusable element is inside an aria-hidden container cover the opposite failure direction in the same visibility-mismatch family: content incorrectly hidden or made unreachable, rather than content that should be hidden but isn’t.
Decorative image is not hidden from assistive technology is the equivalent visual-versus-accessibility-tree mismatch applied to permanently decorative images instead of temporarily closed interactive components.
Focused element is hidden behind a sticky header or footer shares this rule’s underlying theme of visual state and focus/accessibility state falling out of sync, applied to occlusion by a sticky element instead of a closed component.
Modal is missing focus management is the natural companion fix: once a modal correctly stays hidden from the accessibility tree while closed, it also needs focus actively moved into it the moment it opens.
Expandable control is missing aria-expanded covers a related visual-state mismatch on the trigger side: a disclosure button whose announced open/closed state doesn’t match reality, the mirror image of this rule’s hidden-but-announced content.
References
Frequently asked questions
Does opacity: 0 hide an element from screen readers the same way display: none does?
No. opacity: 0 only makes an element visually transparent; it stays fully present in the accessibility tree, and screen readers can still navigate into it and read its contents. Only display: none, visibility: hidden, or the inert/aria-hidden attribute combination actually remove or suppress an element from being announced.
Does adding aria-hidden="true" alone, without inert, fully solve this?
For screen reader announcement, yes: aria-hidden="true" removes the element from the accessibility tree so it is never announced. But without inert (or manually removing tabindex from every focusable descendant), a keyboard user tabbing through the page can still land inside the visually hidden element, since aria-hidden does not affect the keyboard tab order on its own.
Does this rule apply to content that is genuinely decorative and meant to be hidden from everyone?
This specific rule is about interactive components like modals, menus, and dialogs that are only temporarily hidden (closed, not yet opened, or dismissed), not permanently decorative content. Permanently decorative content that should always be hidden from assistive technology has a related but distinct failure; see Decorative image is not hidden from assistive technology.
Does using CSS to move an element off-screen with a large negative margin count as visually hidden for this rule?
Yes. Off-screen positioning removes an element from the visible viewport but, like opacity: 0, leaves it fully present in the accessibility tree unless it also carries aria-hidden or is removed from the DOM. A screen reader user tabbing or navigating by content can still reach an off-screen-positioned but non-hidden element.
Is the fix the same for a dropdown menu as it is for a full-page modal?
The mechanism is identical: add aria-hidden and inert (or remove the element from the DOM) when closed, and remove both when open, regardless of whether the component is a small dropdown menu or a full-page modal. The scale of the component does not change the underlying accessibility-tree requirement.