Quick answer: This audit fails when an element marked aria-hidden="true" still contains a link, button or input that can receive keyboard focus. Usually it is a closed mobile drawer or an off-screen carousel slide. Fix it by adding the inert attribute to the hidden container, or by setting tabindex="-1" on every focusable child.
This Lighthouse audit fails when an element marked aria-hidden="true" still contains something a keyboard can reach: a link, a button, a form field, or anything with tabindex="0". aria-hidden removes content from the accessibility tree. It does not remove it from the tab order. So a keyboard user tabs onto a control they cannot see, and a screen reader user lands on a control that is announced as nothing.
TL;DR
- What: An
aria-hidden="true"element has a focusable child. - Where it comes from: Almost always a closed mobile menu drawer, or the off-screen slides of a carousel.
- Fix: Add
inertto the hidden container. If you cannot, settabindex="-1"on every focusable child. For a closed drawer, hide it from everyone withhiddenordisplay: none.
What does the aria-hidden-focus audit check?
Lighthouse runs the axe-core rule aria-hidden-focus against the rendered DOM. Deque words the same rule "ARIA hidden element must not be focusable or contain focusable elements", so you will see both phrasings in search results and in axe DevTools. They are the same check.
For every element with aria-hidden="true", axe looks for descendants that can receive keyboard focus:
- links with an
href,<button>,<input>,<select>,<textarea>,<summary> - any element with
tabindex="0"or a positivetabindex <iframe>and elements withcontenteditable
A descendant is fine if it has tabindex="-1", is a disabled form control, is hidden with display: none or visibility: hidden, or sits inside an inert subtree. Position does not matter: a slide moved off-screen with transform is still focusable, and still fails.
The failing title reads [aria-hidden="true"] elements contain focusable descendents. The passing title is [aria-hidden="true"] elements do not contain focusable descendents. "Descendents" is Lighthouse's own spelling, so search for it that way.
What does "Focusable content should have tabindex="-1" or be removed from the DOM" mean?
When you expand a failing element in the Lighthouse report, axe-core prints one of these blocks under it:
Fix all of the following:
Focusable content should be disabled or be removed from the DOM
Focusable content should have tabindex="-1" or be removed from the DOM
The offender shown is the aria-hidden="true" container, not the focusable child, which is why the snippet often looks harmless: <div class="mobile-drawer" aria-hidden="true">. The problem is inside it.
"Fix all of the following" is literal, unlike the "fix any" block under button-name and link-name. Every focusable child must be dealt with. The first line covers form controls (disabled takes them out of focus). The second line covers links and buttons (tabindex="-1"). "Removed from the DOM" means do not render the hidden content at all until it is shown. Adding inert to the container satisfies both lines in one attribute, because axe-core has treated inert content as not focusable since version 4.7.
Why does aria-hidden with focusable content matter?
- Keyboard users lose their place. Tab moves focus into an invisible drawer or a slide three positions off-screen. Focus disappears and the next several presses do nothing visible.
- Screen reader users hit ghost controls. The control receives focus but has been removed from the accessibility tree. Depending on the browser and screen reader, it is announced as nothing, as just "link", or out of context.
- WCAG conformance. It can fail SC 4.1.2 Name, Role, Value and SC 2.4.3 Focus Order, both Level A.
- Lighthouse score.
aria-hidden-focuscarries a weight of 7 in the accessibility category in Lighthouse 13.5. It is pass or fail for the whole page, so one hidden drawer costs the full 7.
How do I fix "[aria-hidden="true"] elements contain focusable descendents"?
Choose by what the hidden content is.
1. Content hidden from everyone (closed drawer, closed modal, collapsed menu). Do not use aria-hidden alone. Hide it for all users, which also removes it from the tab order:
<!-- FAILS: invisible, but its links are still in the tab order -->
<nav class="mobile-drawer" aria-hidden="true">
<a href="/shop">Shop</a>
<a href="/about">About</a>
</nav>
<!-- FIX: hidden from everyone while closed -->
<nav class="mobile-drawer" id="drawer" hidden>
<a href="/shop">Shop</a>
<a href="/about">About</a>
</nav>
If the drawer needs a slide-in animation, display: none breaks it. Use visibility: hidden on the closed state instead (it animates when paired with a transition on transform), or add inert while closed.
2. Content that is only visually off-screen (carousel slides). Make inactive slides inert, and toggle it as slides change:
<!-- FAILS: inactive slide hidden from screen readers, links still focusable -->
<div class="slide" aria-hidden="true" style="transform: translateX(200%)">
<a href="/products/linen-shirt">Linen shirt</a>
</div>
<!-- FIX: inert removes focus and hides it from assistive tech in one attribute -->
<div class="slide" inert style="transform: translateX(200%)">
<a href="/products/linen-shirt">Linen shirt</a>
</div>
// On slide change
slides.forEach((slide, i) => {
slide.inert = i !== activeIndex;
});
inert is supported in all current browsers (Chrome, Edge, Firefox and Safari since 2023), and it also blocks mouse clicks and text selection on the hidden slide, which is what you want.
3. When you cannot add inert (a third-party widget you only control through callbacks), take each focusable child out of the tab order while hidden, and restore it when shown:
const FOCUSABLE = 'a[href], button, input, select, textarea, summary, iframe, [tabindex]';
function setHidden(container, hidden) {
container.setAttribute('aria-hidden', String(hidden));
container.querySelectorAll(FOCUSABLE).forEach((el) => {
if (hidden) {
if (el.hasAttribute('tabindex')) el.dataset.prevTabindex = el.getAttribute('tabindex');
el.setAttribute('tabindex', '-1');
} else if ('prevTabindex' in el.dataset) {
el.setAttribute('tabindex', el.dataset.prevTabindex);
delete el.dataset.prevTabindex;
} else {
el.removeAttribute('tabindex');
}
});
}
Saving the previous value matters: a blanket removeAttribute('tabindex') on show would break any child that had a deliberate tabindex.
What are the most common causes on a real site?
In a scan of 269 production storefronts, aria-hidden-focus failed on 21.6% of them (58 sites). Re-running live on failing stores, the offenders were the same two shapes:
- Carousel slides. Slider libraries such as Flickity, Slick and Swiper mark inactive slides
aria-hidden="true"and move them off-screen withtransform: translateX(400%), but leave the product links and buttons inside them focusable. One store failed on seven offenders from a single collection carousel, a gallery and a blog-post slider. Every inactive slide is a separate offender. - Closed mobile drawers. A menu or cart drawer marked
<div class="mobile-drawer" aria-hidden="true">while closed, but moved off-canvas rather than hidden, so every menu link inside it stays in the tab order. - Duplicated content for marquees and infinite scrollers. Logo strips and announcement tickers clone their items for a seamless loop and mark the clones
aria-hidden="true", keeping the links in the clones focusable. - Decorative wrappers.
aria-hidden="true"added to a card or icon wrapper to silence a noisy graphic, where the wrapper also contains the card's link.
How do I fix this in React, Next.js, WordPress, or Shopify?
React / Next.js
React 19 accepts inert as a boolean prop. Older React versions warn on it, so set it as a string or through a ref:
// React 19
<div className="slide" inert={!isActive}>...</div>
// React 18 and earlier
<div className="slide" inert={!isActive ? '' : undefined}>...</div>
For drawers and dialogs, prefer the native <dialog> element or a library that manages focus (Radix, React Aria, Headless UI). They do not leave closed content focusable. Catch regressions with @axe-core/playwright in end-to-end tests: the failing state only exists after the component renders, so static linting misses it.
WordPress
- Slider plugins. Smart Slider, Slider Revolution, MetaSlider and slick-based theme sliders are the usual source. Update first, then re-run Lighthouse. If it still fails, add
inertto inactive slides in the plugin's slide-change callback. - Theme mobile menus. If the off-canvas menu is moved with
transformand only markedaria-hidden, addvisibility: hiddento the closed state in your child theme CSS. - Logo carousels and tickers. Elementor and similar builders clone items for looping marquees. Check the clones in DevTools; if they contain links, prefer a non-looping layout or a widget that sets
tabindex="-1"on clones.
Shopify
The failing stores we re-ran used theme sliders built on libraries such as Flickity. Whatever the library, toggle inert when the selected slide changes. For Flickity, use its change event:
flkty.on('change', () => {
flkty.cells.forEach((cell, i) => {
cell.element.inert = i !== flkty.selectedIndex;
});
});
Check theme sections and app-injected sections separately: the offender snippet's class names usually show which one it is. If the offender comes from an app, the fix belongs to the app developer, so report it to them with the Lighthouse snippet.
What aria-hidden-focus pitfalls should I avoid?
- Do not set
aria-hidden="false"on the child. It does not undo a hidden ancestor. The child stays hidden and the audit still fails. - Do not use
aria-disabled="true"instead ofdisabled.aria-disabledannounces the state but does not remove focus. - Do not put
aria-hidden="true"on the focusable element itself.<button aria-hidden="true">fails this rule directly, and leaves a focusable control with no name. - Do not leave
tabindex="-1"on after the content is shown. The slide becomes visible but unreachable by keyboard. Restore focusability when the content appears, or useinert, which you toggle in one place. - Do not fix a drawer by moving it further off-screen.
left: -9999pxis still in the tab order. Position is not visibility. - Do not add
aria-hidden="true"to<body>or a page wrapper while a modal is open without also making that background inert. That is the most common way to introduce this failure while fixing a different one.
How do I verify the fix?
- Re-run Lighthouse. The
aria-hidden-focusaudit should pass and move into "Passed audits". - Press Tab from the top of the page with every drawer closed and every carousel at rest. Focus must never disappear or land on something off-screen. Then open the drawer and advance the carousel, and tab again.
- In Chrome DevTools, select a hidden slide and check the Accessibility pane. An inert or hidden subtree should show as ignored, and its children should not be in the tab order.
- Check at mobile width as well as desktop. Off-canvas drawers usually only exist at small breakpoints, so a desktop-only check misses them.
- Add axe DevTools or an axe check in your end-to-end tests, run after interacting with the slider, to catch regressions before they ship.
Related audits
- Buttons do not have an accessible name, the other failure that icon-only slider controls cause
- Links do not have a discernible name, unlabeled links inside slides and drawers
- Document does not have a main landmark, page structure for screen reader navigation
- Tap targets, slider arrows and dots that are too small to hit
Audit your URL at https://lighthouse-md.com.
Audit your page now
Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.