Skip to content

tap-target 2.5.8 spacing: three narrow false-exemptions (inline-neighbour, diagonal, nested-skip) #4

Description

@ikraamg

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions