# [aria-hidden="true"] elements contain focusable descendents: what it means and how to fix it

**Audit ID:** `aria-hidden-focus` · **Category:** Accessibility

<!--QA-->
> **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.
<!--/QA-->

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 `inert` to the hidden container. If you cannot, set `tabindex="-1"` on every focusable child. For a closed drawer, hide it from everyone with `hidden` or `display: none`.

## What does the aria-hidden-focus audit check?

Lighthouse runs the axe-core rule [`aria-hidden-focus`](https://dequeuniversity.com/rules/axe/4.13/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 positive `tabindex`
- `<iframe>` and elements with `contenteditable`

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](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) and [SC 2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html), both Level A.
- **Lighthouse score.** `aria-hidden-focus` carries 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:

```html
<!-- 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:

```html
<!-- 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>
```

```js
// 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:

```js
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 with `transform: 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:

```jsx
// 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 `inert` to inactive slides in the plugin's slide-change callback.
- **Theme mobile menus.** If the off-canvas menu is moved with `transform` and only marked `aria-hidden`, add `visibility: hidden` to 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:

```js
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 of `disabled`.** `aria-disabled` announces 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 use `inert`, which you toggle in one place.
- **Do not fix a drawer by moving it further off-screen.** `left: -9999px` is 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?

1. Re-run Lighthouse. The `aria-hidden-focus` audit should pass and move into "Passed audits".
2. 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.
3. 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.
4. Check at mobile width as well as desktop. Off-canvas drawers usually only exist at small breakpoints, so a desktop-only check misses them.
5. Add [axe DevTools](https://www.deque.com/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](/audits/button-name), the other failure that icon-only slider controls cause
- [Links do not have a discernible name](/audits/link-name), unlabeled links inside slides and drawers
- [Document does not have a main landmark](/audits/document-main-landmark), page structure for screen reader navigation
- [Tap targets](/audits/tap-targets), slider arrows and dots that are too small to hit

---

Audit your URL at https://lighthouse-md.com.
