a11y/interactive-nesting · Interactive element nested in an interactive element
An interactive element should not sit inside another interactive element.
Severity: warning · Category: a11y
What it checks
Flags an interactive element (<button>, <input>, a literal interactive role, …) found nested inside another interactive container.
Only three kinds of element open a container that this rule watches for a nested descendant:
<a href="…">. An href-less<a>is not a container.<button>.- An element with a literal role ARIA marks children-presentational, meaning user agents should not expose its descendants through the accessibility API:
button,checkbox,radio,switch,tab,menuitemcheckbox,menuitemradio,option,slider,scrollbar.linkis a container too, not because ARIA marks it children-presentational, which it does not, but because this rule applies to it the same nesting restriction<a href>carries in the HTML content model. Roles that legitimately contain their own controls are not containers:role="gridcell"holding a button is the documented grid pattern, and an ARIA 1.1role="combobox"wraps its own<input>.
Any interactive element entering while one of those containers is open is flagged, for example:
<a href="/x">
<button>Go</button>
</a>
Not flagged:
- A descendant whose only claim to interactivity is its
tabindex, when thattabindexis negative (e.g.<div tabindex="-1">) — it is removed from the tab order and does not compete for keyboard focus. A natively interactive descendant such as<button tabindex="-1">is still flagged: it stays clickable, and the container’s content model forbids it regardless of tab order. - A descendant of an href-less
<a>, since a plain<a>with nohrefis not itself interactive. - Interactive elements nested across components (e.g. a
<button>inside a child component rendered inside an<a href>). This rule only sees a single component’s own template, so that variant is a known non-goal. - Namespace is not tracked: an
<a href>inside<svg>is classified as the HTML<a>, so an SVG link wrapping a<button>is reported as interactive nesting. That is a parser approximation rather than an SVG restriction, since SVG itself forbids only nested SVG links, but the pattern is one no reader of the HTML rule would want either, so the finding stays.
Why it matters
Keyboard and assistive-technology users navigate by control, not by DOM position. A control nested inside another control is announced and operated inconsistently: browsers disagree on which element a click or Enter activates, and screen readers differ on how the pair is presented. When the outer element is an <a href> or a <button>, it is also invalid HTML: their content models forbid interactive descendants outright. A role-based container such as role="button" on a <div> is not covered by that content-model rule, but its interaction model breaks the same way.
How to fix
Restructure the markup so each interactive control is a sibling, not a descendant, of another:
<div>
<a href="/x">Go to x</a>
<button>Extra action</button>
</div>
Mode differences
None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.
Disabling
If the nesting is intentional and handled some other way (e.g. pointer-events and a synthetic focus trap), silence a single element with <!-- svelte-vitals-disable-next-line a11y/interactive-nesting -->, or turn the rule off:
export default {
rules: {
'a11y/interactive-nesting': 'off'
}
};