( Robust / WCAG 4.1.2 )
Interactive controls are nested inside each other
What is this issue?
An interactive element, most often a <button>, an <a> with an href, or an element carrying an interactive ARIA role such as role="button", contains another interactive element nested inside its DOM structure, for example <button><a href="/details">View details</a></button>. Both the outer and inner elements are independently focusable and independently activatable in native HTML, so nesting them creates two competing controls occupying the same visual and structural space.
The browser’s accessibility tree is built from this same markup, and it has no reliable way to represent “two separate controls, one inside the other” as two separate stops with two separate purposes. Something has to give, and which control gives depends on the browser and the assistive technology reading it.
Why does this matter?
When two interactive controls are nested, browsers resolve the ambiguity differently: some fire only the outer element’s handler, some fire the inner one’s, and a few fire both. A screen reader building its own version of the accessibility tree from that same markup often collapses the pair into a single announced control, silently dropping the action a sighted mouse user can still see and click.
A shopper who taps “View details” inside a card-wide link might trigger the card’s own navigation instead, landing on a page they didn’t ask for, with no way to predict in advance which control actually wins. On a product listing with dozens of cards built the same way, that unpredictability repeats on every single item.
Who is affected?
- Screen reader users: the accessibility tree may expose only one of the two nested controls, so the inner button’s separate action can disappear entirely from what NVDA or VoiceOver announces, leaving no way to reach it at all.
- Keyboard users: a nested pair can produce two tab stops for what looks like one control, wasting a Tab press, or collapse into a single stop that only ever activates the outer element, making the inner action unreachable by keyboard.
- Voice control users: voice software matches a spoken command like “click view details” against whichever control’s accessible name the browser decided to expose, so the command can activate the wrong control or find nothing to match at all.
What users experience
Diego navigates entirely by keyboard because a repetitive strain injury makes using a mouse painful. He tabs through a product listing built as <button class="add-to-cart"><a href="/details">View details</a></button> for each card. Chrome collapses the pair into a single tab stop, and pressing Enter fires the outer button’s “add to cart” handler instead of following the link. Diego adds three items to his cart by accident while trying to read their details, then has to remove them one at a time before he can keep browsing.
How do I fix it?
Flatten the markup so the two controls sit as siblings instead of one nested inside the other. This works because it removes the ambiguity at its source: with no nesting, there’s only ever one interactive element in that part of the accessibility tree, and every browser and screen reader agrees on what it is and what it does.
Decide which action is primary for that part of the page, and keep the other as a separate, adjacent control rather than a child of the first. For a “clickable card” pattern where the whole card should feel clickable, use a CSS technique that visually stretches a single link’s click target across the card (an absolutely-positioned ::after pseudo-element covering the card, tied to one real anchor) instead of wrapping the card in an anchor and nesting a button inside it. This keeps exactly one interactive element in the DOM while still giving mouse and touch users a large click area.
Code Examples
<button class="add-to-cart">
<a href="/details">View details</a>
</button><!-- Method 1: siblings instead of nested (simplest) -->
<div class="card-actions">
<a href="/details">View details</a>
<button type="button" class="add-to-cart">Add to cart</button>
</div>
<!-- Method 2: one real link, CSS stretches its click area over the card -->
<div class="card">
<h3><a href="/details" class="stretched-link">Wireless keyboard</a></h3>
<button type="button" class="add-to-cart">Add to cart</button>
</div>Both versions keep exactly one interactive element in any given nested position. Method 1 is the direct fix: the link and button become siblings with their own separate tab stops. Method 2 keeps a large, whole-card click target for the primary action (viewing details) by stretching one real link’s hit area with CSS, while the “Add to cart” button stays a fully independent sibling control instead of a nested one.
Framework Examples
A reusable card component is the most common source of this bug in component-driven UIs: a <Card> wrapper adds an onClick handler (or wraps its children in a router <Link>) without knowing that a consumer will later drop a <Button> inside it. The fix has to happen at the component boundary, not by patching one call site.
// Before: Card wraps children in a Link, and a Button gets nested inside it
function ProductCard({ product }) {
return (
<Link to={`/details/${product.id}`}>
<h3>{product.title}</h3>
<button onClick={() => addToCart(product.id)}>Add to cart</button>
</Link>
);
}
// After: Link only wraps the title; the button is a sibling
function ProductCard({ product }) {
return (
<div className="card">
<h3>
<Link to={`/details/${product.id}`}>{product.title}</Link>
</h3>
<button onClick={() => addToCart(product.id)}>Add to cart</button>
</div>
);
}
The framework-specific risk here is that Link and Button come from a shared component library and look like plain layout wrappers at the call site. Nothing in JSX visually warns you that a <Link> renders an <a> and will nest around anything you place inside it, including another interactive component.
Common Mistakes
Mistake: “Adding event.stopPropagation() to the inner button fixes it.” stopPropagation only changes which JavaScript handler fires in the browser you happen to be testing in. It does nothing to the accessibility tree, which still contains two nested interactive roles, so the screen reader experience stays broken even after the click-handling bug looks fixed.
Mistake: “The inner element is small, so there’s no real conflict.” The DOM and accessibility-tree ambiguity has nothing to do with visual size. A small icon-sized button nested inside a page-wide anchor is exactly as broken as two same-sized elements; the browser and screen reader still have to resolve the same nested-role conflict either way.
Mistake: “This is only a screen reader problem, so it’s low priority if most of our users are sighted.” Keyboard and voice control users hit this failure regardless of vision, and the underlying browser click-resolution inconsistency is a functional bug, not only an announcement bug: sighted mouse users on an inconsistent browser can also trigger the wrong action.
Mistake: “Setting tabindex="-1" on the inner element fixes the accessibility problem.” Removing the inner element from the tab order stops keyboard users from reaching it at all, which trades one failure (an ambiguous nested pair) for another (an unreachable control) rather than fixing anything. The inner action still needs to be reachable, as a sibling, not as an unfocusable nested element.
How RedFlag Detects This
Automated: axe-core rule, runs on every scan. RedFlag calls axe-core’s nested-interactive rule as part of every scan, restricted to the WCAG 2.0/2.1/2.2 A and AA rule set. The rule inspects elements with a native or ARIA interactive role (buttons, links with an href, and roles like button, link, checkbox, or combobox) and flags any that contains a descendant carrying its own interactive role.
False negative: the check looks for elements with an actual interactive role or native interactive semantics. A <span onclick="..."> nested inside a real <button> has no role and isn’t natively focusable, so it passes the automated check even though it still competes for the same click. False positive: an inner element marked aria-hidden="true", deliberately removed from the accessibility tree, can still be flagged, even though it no longer creates a double-announcement problem for screen reader users, only a residual risk for sighted mouse users. Manual step: tab to the nested pair in your target browsers and confirm which control actually activates, then check what a screen reader announces at that position.
Manual Testing
- Open the page in Chrome or Firefox with NVDA or VoiceOver running.
- Tab to any card, row, or component you suspect contains nested interactive elements.
- Listen to what’s announced: a correctly flattened pair announces two distinct controls with two distinct names; a nested pair often announces only one, or announces a role that doesn’t match what’s visible.
- Press Enter or Space on the control and confirm it performs the action you expected, then repeat with a mouse click on the same visual area to check whether the browser resolves the click to the same target.
- If the keyboard and mouse activate different actions, or the screen reader only exposes one control, the check fails.
Related WCAG Success Criteria
4.1.2 Name, Role, Value: Every interface component must expose a programmatically determinable name, role, and value to assistive technology. A nested interactive pair produces an ambiguous role at that position in the tree, so at least one of the two controls fails to expose a clean, unambiguous role.
2.1.1 Keyboard: All functionality must be operable through a keyboard interface. When a nested pair collapses to a single tab stop, whichever control loses that collapse becomes unreachable by keyboard entirely, which is a distinct way this rule’s failures also violate 2.1.1.
Related Issues
Button or link has no accessible name covers the opposite structural problem in the same component family: a control with no name at all, rather than two controls competing for one nested position.
Button has no accessible name is the button-specific version of the same naming requirement that a nested pair puts at risk when one control’s role or name gets dropped from the tree.
ARIA role is invalid or misused matters here because nested interactive failures often involve a role="button" or role="link" added to a non-native element, compounding the nesting problem with a role that may not even be valid for its context.
Focus order does not match visual or logical order shares a mechanism with this rule: a collapsed or duplicated tab stop from nested controls is one common way a page’s focus order stops matching what a keyboard user expects to encounter.
Element has a positive tabindex value is a different tab-order failure that frequently shows up in the same card or list components where nested-interactive failures live, since both stem from ad hoc keyboard-navigation patches.
Link text is generic and gives no context commonly co-occurs with the “clickable card” pattern that causes nested-interactive failures, since a card-wide link is often labelled “Read more” or “View details” with no per-card distinction.
References
Frequently asked questions
Does a button inside an anchor ever behave consistently across browsers?
No. Different browsers resolve the click and keypress differently for a nested pair, and some collapse the accessibility tree to expose only one of the two controls. There is no combination of markup or CSS that makes nested interactive elements behave predictably, which is why the fix is always to flatten the structure rather than work around one browser's current behavior.
Does adding event.stopPropagation() to the inner control fix this?
No. stopPropagation only changes which JavaScript click handler fires in the browser you tested in; it does not change what a screen reader announces, and the accessibility tree still contains two nested interactive roles. Flattening the DOM structure is the only fix that addresses the underlying problem for assistive technology.
Is wrapping an input inside its own label element the same violation?
No. A label wrapping its input is the standard implicit-labelling technique, and the label itself does not create a second, independently focusable and activatable control competing with the input. Nested-interactive specifically means two separately operable controls, such as a link and a button, sharing the same nested space.
Does role="presentation" on the outer element fix a nested button and link?
Not by itself. Adding role="presentation" changes what a screen reader announces for the outer element's role, but if the outer element is still a real <button> or <a>, it keeps its native focusability and click behavior. You end up with an announced role mismatch on top of the original double-control problem, not a fix for it.
Is a "clickable card" pattern with a link wrapping the whole card and a button inside it a common cause of this?
Yes, it is one of the most common real-world causes. Wrapping an entire card, including a secondary action button, in one large anchor creates exactly this failure, and it usually needs restructuring (such as making the card link only cover the title, with the button placed as a sibling) rather than a single-attribute fix.