sfw/fix
nested-interactive high

nested-interactive: Interactive controls must not be nested

A focusable control is nested inside another interactive element, so screen readers skip the inner control and its name and role never get announced.

What you see

Interactive controls must not be nested
Element has focusable descendants
Element: <a href="/product"><button>Add to cart</button></a>

What’s actually happening

You've got a button inside a link, or an input inside an element styled as a button. Click the outer thing and the inner thing fires too — or neither does what you expect. Under a screen reader the inner control simply isn't announced as a separate control; the AT exposes only the outer role and folds the inner one into its accessible name. Worse, the HTML parser sometimes rewrites your DOM: an <a> nested inside another <a> gets split into siblings, and a <button> is not allowed to contain interactive content at all, so the browser may hoist the child out.

Common causes

  • A <button> wraps an <a href> (or vice versa) to get both a click handler and a navigation target on the same element
  • A clickable card uses an outer <a> covering the whole tile, then drops a real <button> (favorite, add-to-cart, dismiss) inside it
  • An <input>, <select>, or second <label> control sits inside a <label> that someone also wired up as a clickable region
  • role="button" or role="link" is applied to a div that already contains a native <button>, <a>, or form field
  • A custom dropdown/combobox renders focusable <li> items (tabindex set) inside a parent that is itself a focusable widget

How to fix it

  1. Pick one control per job and un-nest the DOMDecide whether the element navigates (link) or acts (button) and use a single element for it. For a clickable card, make the title an <a> and keep the secondary <button> as a sibling, not a descendant — position them with CSS (the link can use a ::after stretched over the card while the button sits above it with position:relative and a higher z-index).
  2. Replace the wrapper's behavior with event delegation or CSS overlayIf you wrapped a control to enlarge the hit area, drop the wrapper's interactivity. Use a stretched-link pattern (a single anchor with an absolutely-positioned ::after pseudo-element covering the parent) so only one element is actually focusable and clickable.
  3. Move secondary actions out of labelsA <label> should associate text with one form control via for/id. If you need a button next to a checkbox, put the <button> outside the <label> and lay them out with flexbox — never embed a second interactive element inside the label.
  4. Audit custom widgets for stray tabindexIn listboxes, menus, and grids, only the container or the active item should be in the tab order (roving tabindex). Make sure you're not giving both the parent widget and its children tabindex="0", which nests focus stops.
  5. Re-test with Tab and the screen reader rotorTab through the component — each stop should be one distinct control. In VoiceOver, open the rotor (form controls / links) and confirm the inner control shows up as its own item with the right role and name.

Stop it recurring

Treat "one focusable element = one action" as a hard rule; layer hit areas with CSS overlays instead of nesting one control inside another.

Related errors