skip to content

A card component wraps its entire markup in an <a href> and that markup contains a <button>. Why does HTML's content model forbid this, and what would you do instead?

level: seniorimportance: should knowfreq 40%

answer

  1. one activation target per region
  2. the anchor forbids a whole category of descendants
  3. two focus stops, one ambiguous click
  4. the button's label joins the link's name
  5. fix by restructuring, not by attributes

basics

~20 s

The a element's content model forbids interactive content descendants, and button is interactive content, so nesting a control inside a link is invalid. Restructure the markup: link only the non-interactive content, and place the button as a sibling outside the anchor.

solid answer

~50 s

An `a` element's content model is transparent **but with no interactive content descendant, no other `a` descendant, and no descendant carrying `tabindex`**. `button` is interactive content, so a button inside a link is invalid at any depth. The reason is not pedantry: two activatable elements occupying the same region have no defined answer for what a click, an Enter press or a screen-reader activation should do, and the button's text is absorbed into the link's accessible name. The fix is structural, not attribute-based. Keep the anchor around the non-interactive content — the heading, the summary, the image — and move the button out to be a sibling within the card container. If the whole card must feel clickable, the pattern is a single anchor on the title with the card as its container, so exactly one activation target exists per region.

code

html · 15 lines
html
<!-- invalid: button is interactive content inside an a element -->
<a href="/posts/1" class="card">
  <h2>Release notes</h2>
  <p>Everything that changed this month.</p>
  <button type="button">Save for later</button>
</a>

<!-- valid: the anchor wraps only non-interactive content, the button is a sibling -->
<div class="card">
  <a href="/posts/1">
    <h2>Release notes</h2>
    <p>Everything that changed this month.</p>
  </a>
  <button type="button">Save for later</button>
</div>

go deeper

for a junior

Know that a link may not contain a button or any other control, and that the fix is to put the control next to the link rather than inside it.

for a middle

Name the rule — the a element's content model forbids interactive content descendants, nested anchors and any descendant with tabindex — and list which elements count as interactive content.

for a senior

Explain why the nesting is undecidable in practice: ambiguous click target, two focus stops, the control's label absorbed into the link's accessible name, and no parser repair. Then restructure the markup rather than patching with attributes.

for a principal

Own the component contract: a card that exposes a link wrapper plus free-form child slots guarantees someone will slot a control inside the link. Decide whether the component splits link and action regions by API, and how that is enforced rather than documented.

## The rule The HTML specification defines a category called **interactive content**: elements users can interact with directly. Its members are `a` (when it has an `href`), `button`, `details`, `embed`, `iframe`, `label`, `select`, `textarea`, `input` (unless `type="hidden"`), and `audio`/`video` when the `controls` attribute is present. The `a` element's content model is transparent — it accepts what its parent accepts — **but with no interactive content descendant, no `a` element descendant, and no descendant with the `tabindex` attribute specified.** So a card entirely wrapped in an anchor may legally contain headings, paragraphs, images and spans, and may not contain a button, an input, a select, an iframe or another link, however deeply nested. The same prohibition runs the other way for `button`, whose content model is phrasing content **with no interactive content descendant** — so a link inside a button is equally invalid. ## Why the spec bothers Nesting two activatable elements creates a region with two competing activation behaviours and no principled way to resolve them: - **Pointer.** A click lands on the button, but the event continues up through the anchor, which also has activation behaviour. What the user gets depends on what the button's own handling does, which is exactly the kind of thing that differs between implementations and breaks on refactor. - **Keyboard.** Both elements are focusable, so tabbing puts the user inside a link, then inside a button that lives inside that link, with no announced boundary explaining the relationship. - **Accessible name.** A link's name is computed from its contents. The button's label is text inside the anchor, so it gets swept into the link's name — the link ends up announced as "Release notes Everything that changed this month Save for later", which is useless in a list of links. - **Assistive-technology navigation.** Users who list all links or all buttons on a page get entries whose boundaries do not match anything visible. The parser also does not clean this up for you the way it does for nested anchors. `<a><a>` is repaired — a second `a` start tag closes the first — so nested links end up as siblings. A `button` inside an `a` is simply left as authored: the DOM keeps the invalid structure, and the runtime confusion is real rather than theoretical. ## What not to do The reflex fixes all make it worse: - Swapping the inner `<button>` for a `<div>` with a click handler removes the validity error and the keyboard access at the same time. The control is now unreachable for anyone not using a mouse. - Adding `tabindex` anywhere inside the anchor is explicitly called out by the anchor's content model as forbidden. - Swapping the outer anchor for a `<div>` with a click handler destroys the link: no middle-click, no open-in-new-tab, no copy-link, no crawlable destination, and the region disappears from the page's list of links. Every one of these trades a validation error for a real user-facing regression. ## The restructure The honest fix is to stop overlapping the regions. ```html <div class="card"> <a href="/posts/1"> <h2>Release notes</h2> <p>Everything that changed this month.</p> </a> <button type="button">Save for later</button> </div> ``` The anchor wraps only non-interactive content — legal, because the anchor's transparent content model inherits the container's permission to hold flow content. The button is a sibling inside the card container. Now there are exactly two activation targets, each with its own boundary, its own accessible name and its own tab stop. When the design insists the entire card surface be clickable, the markup pattern is the same shape: one anchor, on the card's title, with the card container around it and the secondary controls as siblings. The card-wide hit area is then a presentation concern layered on top of markup that is already correct, rather than something the markup has to fake by swallowing the controls. ## What an interviewer is checking That you recognise it as a content-model violation with a named rule, not a lint nit; that you can say why two nested activation targets are undecidable rather than just "it's bad practice"; and that your fix restructures the markup instead of trading a real element for a div plus attributes. Reaching for `tabindex` or a click-handling div here is the answer that fails the question.

  • Which elements count as interactive content, so that none of them may appear inside an <a>?
    `a` with an `href`, `button`, `details`, `embed`, `iframe`, `label`, `select`, `textarea`, `input` unless its type is `hidden`, and `audio` or `video` when `controls` is present. The anchor additionally forbids any descendant carrying a `tabindex` attribute, so you cannot make a nested element focusable by hand either.
  • Is a nested <a> inside an <a> repaired by the parser the way a div inside a p is?
    Yes, and that is the difference worth knowing. An `a` start tag encountered while another anchor is open closes the first one, so nested links come out of the parser as siblings. A `button` inside an `a` is not repaired at all — the DOM keeps the invalid nesting, which is why that case actually misbehaves at runtime.
  • Why is swapping the inner button for a div with a click handler a worse answer, not a fix?
    Because it trades a validation error for an accessibility regression. A `div` is not focusable and does not activate on Enter or Space, so keyboard and screen-reader users lose the control entirely, and it no longer appears when a user lists the page's buttons. The invalid markup at least exposed a real control; the div does not.
  • How do you keep the whole card clickable once the button is a sibling of the link?
    Keep exactly one anchor, on the card's title, inside the card container, with the secondary controls as siblings. The card-wide hit area then becomes a presentation layer over markup that is already correct — one link, one button, each with its own accessible name and tab stop — instead of markup that swallows the controls to fake the effect.

saying these in an interview costs you the question

  • It is invalid but harmless since browsers render it fine
  • Replace the inner button with a div and a click handler
  • Add tabindex to sort out the focus order
  • Make the outer wrapper a div with an onclick instead of a link
  • Only nested anchors are forbidden, other controls are fine

context