The WCAG 2.5.8 spacing exception (branch tap-target-spacing-exception) is faithful for the common axis-aligned and full-size cases, but an adversarial review found three narrow false-exemptions — cases the exemption lets through that 2.5.8 would flag. None are regressions on the shipped fixtures; capturing them so they aren't lost. Ordered by how likely they are to bite.
1. Inline-exempt links don't count as crowding neighbours (most realistic)
tapTargets pass 1 drops an inline-exempt link (the WCAG 2.5.8 inline case) from the target set entirely, so it can't crowd anyone. But a visible inline text link is a real tappable region — a small icon jammed against prose text IS crowded, and the code can't see it.
Live repro: an inline-exempt "docs" text link + an adjacent 16×16 icon-only link → tap: [] (icon falsely exempt). Swap the neighbour to a counted 20×16 link and the icon flags. The only variable was whether the neighbour is inline-exempt.
Fix (matches the pass-1/pass-2 split already there): collect EVERY visible interactive box as the neighbour set, and gate only which ones are evaluated as targets by the exemptions. sr-only inputs stay excluded from neighbours (invisible/1px); a visible inline link should not.
2. Diagonal undersized pair — the undersized branch drops the box clause
crowded()'s undersized-vs-undersized branch does only the circle-circle test (centre distance < 24) and omits the spec's other clause: the 24px circle must also not intersect the neighbour's box. For an undersized target big enough that its corners poke past its own 24px circle (any side > ~17px), a pure-diagonal layout slips through.
Live repro: A=4×4 @ (0,0), B=22×22 @ (10,10) (both undersized, 6px apart, no overlap). Centre distance 26.87 ≥ 24 → both exempted, but a 24px circle on A reaches B's nearest corner at 11.31 < 12 → 2.5.8-strict flags A. The overlap check doesn't catch it either.
Fix: in crowded(), run the circle-vs-box test for EVERY neighbour (undersized or not), and additionally the circle-vs-circle test for undersized neighbours. Crowded if either fires.
3. Geometric nested-skip catches sibling overlays (lowest priority)
boxContains skips a pair when one box geometrically contains the other, to avoid a target crowding a nested interactive child. But it's geometric, not DOM: a 16×16 close button positioned inside a large card link's bounds (a sibling overlay, position:absolute) is skipped, and overlap also exempts it (non-static = layering opt-in), so it's fully hidden. The comment "rare, invalid HTML" undersells a sibling overlay.
Fix (if pursued): skip on DOM ancestry, not geometric containment — needs the ancestor chain threaded in, so heavier than #1/#2.
The WCAG 2.5.8 spacing exception (branch
tap-target-spacing-exception) is faithful for the common axis-aligned and full-size cases, but an adversarial review found three narrow false-exemptions — cases the exemption lets through that 2.5.8 would flag. None are regressions on the shipped fixtures; capturing them so they aren't lost. Ordered by how likely they are to bite.1. Inline-exempt links don't count as crowding neighbours (most realistic)
tapTargetspass 1 drops an inline-exempt link (the WCAG 2.5.8 inline case) from the target set entirely, so it can't crowd anyone. But a visible inline text link is a real tappable region — a small icon jammed against prose text IS crowded, and the code can't see it.Live repro: an inline-exempt "docs" text link + an adjacent 16×16 icon-only link →
tap: [](icon falsely exempt). Swap the neighbour to a counted 20×16 link and the icon flags. The only variable was whether the neighbour is inline-exempt.Fix (matches the pass-1/pass-2 split already there): collect EVERY visible interactive box as the neighbour set, and gate only which ones are evaluated as targets by the exemptions. sr-only inputs stay excluded from neighbours (invisible/1px); a visible inline link should not.
2. Diagonal undersized pair — the undersized branch drops the box clause
crowded()'s undersized-vs-undersized branch does only the circle-circle test (centre distance < 24) and omits the spec's other clause: the 24px circle must also not intersect the neighbour's box. For an undersized target big enough that its corners poke past its own 24px circle (any side > ~17px), a pure-diagonal layout slips through.Live repro:
A=4×4 @ (0,0),B=22×22 @ (10,10)(both undersized, 6px apart, no overlap). Centre distance 26.87 ≥ 24 → both exempted, but a 24px circle on A reaches B's nearest corner at 11.31 < 12 → 2.5.8-strict flags A. Theoverlapcheck doesn't catch it either.Fix: in
crowded(), run the circle-vs-box test for EVERY neighbour (undersized or not), and additionally the circle-vs-circle test for undersized neighbours. Crowded if either fires.3. Geometric nested-skip catches sibling overlays (lowest priority)
boxContainsskips a pair when one box geometrically contains the other, to avoid a target crowding a nested interactive child. But it's geometric, not DOM: a 16×16 close button positioned inside a large card link's bounds (a sibling overlay,position:absolute) is skipped, andoverlapalso exempts it (non-static = layering opt-in), so it's fully hidden. The comment "rare, invalid HTML" undersells a sibling overlay.Fix (if pursued): skip on DOM ancestry, not geometric containment — needs the ancestor chain threaded in, so heavier than #1/#2.