Aria-hidden elements must not contain focusable content
Last updated:
Related Guides
aria-hidden elements must not contain focusable content
Elements with aria-hidden="true" cannot receive focus or contain any focusable descendants. This often appears in modals, menus, carousels, and decorative wrappers. Keyboard and screen reader users are affected when hidden regions take focus or hide interactive controls.
Why It Matters
When aria-hidden is true, the element and all its children are removed from the accessibility tree. If anything inside can still be focused, assistive technologies cannot perceive it, while keyboard users can still land on it. That split creates lost context, broken focus order, and inaccessible controls.
People who navigate by keyboard (mobility impairments) may tab into a region that screen readers cannot announce. Screen reader users (blind or low-vision) miss controls entirely if they remain hidden from the accessibility API.
Common Causes
- Placing links, buttons, or inputs inside a container with
aria-hidden="true". - Moving content off-screen with CSS (e.g., large negative positioning) but leaving it focusable.
- Using
aria-disabledinstead of disabled on native controls, which does not remove focus. - Adding
tabindex="0"(or leaving default focusability) within anaria-hiddenregion. - Trying to override a hidden ancestor with
aria-hidden="false"on a child (it won’t re-expose the child to the accessibility API). - Toggling visibility with CSS only, without updating
aria-hiddenor focusability.
How to Fix
- Decide what should be hidden and from whom:
- If content should be hidden from everyone, remove it from layout and focus order (hidden attribute or display:none). Avoid leaving focusable elements in the DOM.
- If content must be visually present but ignored by assistive tech (rare), ensure an equivalent control is accessible elsewhere and make all descendants unfocusable.
- Never place active, focusable controls inside an
aria-hiddencontainer. Move them outside or change the interaction model. - Make descendants unfocusable when necessary:
- For custom elements: set
tabindex="-1"and remove any JS that moves focus inside the hidden region. - For anchors: remove href (or replace with
role="button"andtabindex="-1"when hidden) and re-enable properly when shown. - For native controls: use the disabled attribute (not
aria-disabled) when appropriate. - Do not rely on
aria-hidden="false"on a descendant to counter an ancestor’saria-hidden="true". Toggle the ancestor or restructure the DOM. - Keep ARIA values valid and minimal:
aria-hiddenaccepts only "true" or "false". Use role="presentation"/"none" for purely decorative elements. - Manage focus on show/hide:
- When hiding a component (set
aria-hidden="true"and/or hidden), move focus to the next logical control. - When showing it, set
aria-hidden="false"and place focus on a meaningful element inside.
Standards alignment (WCAG 2.2): 4.1.2 Name, Role, Value; 2.1.1 Keyboard; 2.4.3 Focus Order; 1.3.1 Info and Relationships.
How to Test
Keyboard check:
- Tab through the page. Focus must never land inside elements with
aria-hidden="true". - Open/close modals, menus, and drawers. When closed, their controls must not receive focus.
Screen reader check:
- With VoiceOver, NVDA, or JAWS, navigate by headings, landmarks, and tab. Hidden regions should not be announced. No controls should exist only inside
aria-hiddencontainers.
Mobile/touch check:
- With TalkBack or VoiceOver, explore by touch and swipe. Hidden containers must not be announced or reachable.
Code/DOM check:
- Inspect elements with
[aria-hidden="true"]. Ensure no descendants match focusable selectors:a[href], button, input, select, textarea, summary,[tabindex]:not([tabindex="-1"]),[contenteditable="true"].
Optional quick script to flag violations in dev tools:
const hiddenRegions = document.querySelectorAll('[aria-hidden="true"]');
const focusableSel = 'a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
hiddenRegions.forEach(r => {
const offender = r.querySelector(focusableSel);
if (offender) console.warn('Focusable inside aria-hidden region:', offender);
});Good Example
A menu stays out of the accessibility tree and focus order while hidden, then becomes available and focusable when shown.
<button id="toggle">Menu</button>
<nav id="site-menu" aria-hidden="true" hidden>
<ul>
<li><a href="/products">Products</a></li>
<li><a href="/pricing">Pricing</a></li>
</ul>
</nav>
<script>
const btn = document.getElementById('toggle');
const menu = document.getElementById('site-menu');
btn.addEventListener('click', () => {
const open = menu.hasAttribute('hidden');
if (open) {
menu.removeAttribute('hidden');
menu.setAttribute('aria-hidden', 'false');
menu.querySelector('a').focus();
} else {
menu.setAttribute('aria-hidden', 'true');
menu.setAttribute('hidden', '');
btn.focus();
}
});
</script>Bad Example
Focusable controls live inside an aria-hidden container, and a child tries to override the ancestor’s hidden state.
<aside id="promo" aria-hidden="true">
<a href="/deal">Get the deal</a>
<div aria-hidden="false">
<button type="button">Apply now</button>
</div>
</aside>Quick Checklist
- No focusable descendants inside elements with
aria-hidden="true". - Hidden components use hidden or display:none when they contain interactive elements.
- Do not use
aria-disabledto remove focus; use disabled ortabindex="-1"appropriately. - Do not attempt to override an ancestor’s
aria-hiddenwith a child. - Update
aria-hiddenand visibility together when showing/hiding UI. - Manage focus when toggling components open/closed.
- Validate ARIA values and keep ARIA usage minimal and accurate.