Links and buttons Rule

Do not put one control inside another nested-interactive

A button, a tab, a switch and similar controls are announced as one thing; whatever is inside them is treated as their label, not as separate controls. Put a link or another button inside one and screen readers may never announce the inner control, while a keyboard user can focus something that does not work. Keep one control per control.

Websight reports this as Interactive controls must not be nested

Who it affects

Someone using a screen reader on a gym's class timetable lands on a clickable class card and hears it as one button, so the Book link inside it is never announced.

Someone using a keyboard tabs to the inner Book link, presses Enter, and the outer card's handler fires instead, opening the class details rather than booking.

Someone using a switch or voice control gets two overlapping targets with one name and cannot pick the inner one.

Broken versus fixed

Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.

Broken

The whole card is one button to a screen reader. The Book link inside it is swallowed into the button's name.

Fixed

The card is plain content. The Book link is a separate, named link that keyboard and screen reader users can reach.

Show the markup behind both examples
Broken
<style>body{font-family:system-ui;margin:0;padding:16px}.card{border:1px solid #d0d7de;border-radius:8px;padding:12px;max-width:320px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}.card:focus{outline:3px solid #1b5e9e}.book{background:#1b5e9e;color:#fff;padding:6px 12px;border-radius:6px;text-decoration:none}.book:focus{outline:3px solid #e07a1f}</style>
<div class="card" role="button" tabindex="0">
  <span><strong>Spin</strong><br>6pm, Studio 2</span>
  <a href="#book-spin" class="book">Book</a>
</div>
<p style="font-size:14px;color:#555">Click in this frame and press Tab twice: two stops, one name.</p>
Fixed
<style>body{font-family:system-ui;margin:0;padding:16px}.card{border:1px solid #d0d7de;border-radius:8px;padding:12px;max-width:320px;display:flex;justify-content:space-between;align-items:center}.book{background:#1b5e9e;color:#fff;padding:6px 12px;border-radius:6px;text-decoration:none}.book:focus{outline:3px solid #e07a1f}</style>
<div class="card">
  <span><strong>Spin</strong><br>6pm, Studio 2</span>
  <a href="#book-spin" class="book">Book Spin, 6pm</a>
</div>
<p style="font-size:14px;color:#555">Click in this frame and press Tab: one stop, the link.</p>

Test it by hand in two minutes

  1. Press Tab through the timetable. If one card takes two Tab presses to get past, two controls are stacked, and the inner one is probably nested.
  2. Right click the inner control, choose Inspect, and walk up its ancestors in the Elements panel. If you meet a button, a role="button", a tab, a menuitem, an option or a switch before you leave the card, that is the problem.
  3. With a screen reader running, move to the card. If you hear one long button name that includes the word Book, the link is being swallowed.
  4. Press Enter on the inner control and watch which action fires.

The fix

Before
<div role="button" tabindex="0" class="class-card">
  Spin, 6pm, Studio 2
  <a href="/book/spin-1800">Book</a>
</div>
After
<div class="class-card">
  <h3>Spin, 6pm, Studio 2</h3>
  <a href="/book/spin-1800">Book Spin, 6pm</a>
</div>

Decide what the one action is and make that the control. If the whole card should be clickable, stretch the link over the card with CSS rather than wrapping the card in a button.

Remove button inside a filter chip button

Before
<button class="chip">Yoga <button class="remove">x</button></button>
After
<span class="chip">Yoga <button class="remove" aria-label="Remove Yoga filter">x</button></span>

The chip did not need to be a button; only the remove action does anything.

Whole card clickable with one link

Before
<div role="button" tabindex="0" class="card">
  <a href="/classes/yoga">Yoga</a>
  <p>Tuesdays, 7pm</p>
</div>
After
<div class="card">
  <a href="/classes/yoga" class="stretched">Yoga</a>
  <p>Tuesdays, 7pm</p>
</div>
<style>.card{position:relative}.stretched::after{content:"";position:absolute;inset:0}</style>

The link's ::after covers the card, so a click anywhere follows the link, while the page has one control with one name.

Close button inside a tab

Before
<div role="tab" tabindex="0">Monday <button>x</button></div>
After
<div class="tab-wrap">
  <div role="tab" tabindex="0">Monday</div>
  <button aria-label="Close Monday tab">x</button>
</div>

A tab's children are presentational, so the button inside it is lost. Put it beside the tab.

Common mistakes

  • Adding tabindex="-1" to the inner control to hide the problem; it is still inside, and still unreachable for some people.
  • Putting role="presentation" on the inner button; a focusable element cannot be presentational.
  • Wrapping a whole card in a link and placing buttons inside it; browsers tolerate it, assistive technology does not.
  • Fixing the double click with stopPropagation in JavaScript and leaving the structure as it was.

WCAG criteria this rule maps to

axe-core tags this rule as WCAG 2.2 level A. It is one of the checks for:

The scanner can see a focusable element inside a button, tab, switch, slider, option, menuitem or similar role whose children are treated as presentational. It does not flag a button inside a link, which is also wrong, so check those by hand. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.

Scan your own page for this and 63 other checks

Open any page in Chrome, click Websight, and get a score out of 100 with every issue explained and a draft fix snippet per element.

Add Websight to Chrome

Free scans, no account, nothing leaves your computer. Automated tools find only part of the problems; the extension says so on every report.