Orange, black and white 3D editorial illustration: a smartphone whose screen shows three small dark buttons crowded together above one large orange button surrounded by a glowing spacing ring, with a stylised finger poised over the crowded ones. A Design Den Solutions panel carries the official logo, the headline TOO SMALL TO TAP - MEASURE THE GAP and the line "24 CSS pixels is the floor, spacing is the exception", with +91 98721 90965 and www.designdensolutions.com shown.

Conceptual AI illustration of touch-target size and spacing on a mobile screen. Generated with AI assistance and reviewed for accuracy by Design Den Solutions.

Every conversation about touch targets collapses into one of two camps. One side says "44px or you're failing". The other says "WCAG only requires 24, so we're fine". Both are reasoning about the wrong quantity. SC 2.5.8 is not a size rule. It is a spacing rule with a size floor, and the spacing half is a Euclidean distance test that most audits do not actually compute.

I measured it. Everything below is from a single machine, in a single Chrome build, and the limitations are at the end.

What the criterion actually says

WCAG 2.2, SC 2.5.8 Target Size (Minimum), Level AA:

  • The target's bounding box is at least 24 by 24 CSS pixels, or
  • the Spacing exception applies: an imaginary 24 CSS pixel diameter circle centred on the target's bounding box must not intersect another such circle.

Two circles of diameter 24 do not intersect when and only when their centres are at least 24 apart. So the exception is a centre-to-centre distance test:

passes = (w >= 24 && h >= 24) || min(centreDistance(target, other)) >= 24 

There are four more exceptions — Inline, User agent control, Equivalent and Essential — and automated tooling cannot evaluate any of them. That matters for how you read your own numbers.

For completeness: SC 2.5.5 Target Size (Enhanced) is the AAA criterion at 44 by 44 CSS pixels, and the platform guidance people quote alongside it is 44×44 points on iOS and 48×48 dp on Android. Those are different units on different coordinate systems; they are not the same measurement as a CSS pixel audit.

How I measured

Playwright driving the system Chrome build (157), viewport 390 × 844 CSS px, deviceScaleFactor: 2, isMobile, hasTouch. Two independent methods for the hit-area question, because they can disagree and only one of them is the thing users experience:

  1. document.elementFromPoint(x, y) walked outward from the reported box. Only the element itself or a descendant counts as owned — counting an ancestor would report the parent <div> as the link's hit area, which was my first bug.
  2. A real activation test: attach a click listener, page.mouse.click(x, y) at a known offset, and check whether the handler fired.

The two agreed at every offset tested. That is worth saying, because method 1 alone is a claim about hit-testing and method 2 is a claim about behaviour.

Result 1: the layout box is not the hit area, and most fixes change the box

A 16px inline link and a 14px button, each styled a different way, measured as layout box vs real hit region:

Markup Layout box Measured hit area Clears 24? Clears 44?
bare inline link, 16px 60.2 × 21 60.2 × 21 no no
+ padding: 12px 16px 92.2 × 45 92.2 × 45 yes yes
display:inline-block + same padding 92.2 × 45 92.2 × 45 yes yes
line-height: 2.5 on the link 60.2 × 21 60.2 × 21 no no
line-height: 2.5 on the parent block 60.2 × 21 60.2 × 21 no no
inline-block; min-height: 24px 60.2 × 24 60.2 × 24 yes no
transparent ::after, inset: -8px 60.2 × 21 73.2 × 36 yes no
same ::after with pointer-events: none 60.2 × 21 60.2 × 21 no no
button padding: 4px 8px, 14px 52.7 × 28 52.7 × 28 yes no
button padding: 5px 12px, 14px 60.7 × 30 60.7 × 30 yes no
button min-height/min-width: 44px 64.7 × 44 64.7 × 44 yes yes

A bare 16px text link is 21 CSS pixels tall. It fails the floor by 3 pixels without anyone doing anything wrong. That is the whole bug for most sites.

Result 2: line-height does not make a link easier to hit

This is the one I expected to be wrong about. Giving the link line-height: 2.5 computes to a 40px line box and the containing paragraph really does grow to 40px. But the link's box stayed 50.2 × 21, and the activation window did not move:

Variant Link box Click activates at vertical offsets
default line-height 50.2 × 21 −8, −4, 0, +4, +8
line-height: 2.5 on the link 50.2 × 21 −8, −4, 0, +4, +8
line-height: 2.5 on the parent 50.2 × 21 −8, −4, 0, +4, +8
padding: 12px 16px 82.2 × 45 −16 … +16 (every offset tested)
transparent ::after, inset: -8px 50.2 × 21 −16 … +16 (every offset tested)

At −12 and −16 the paragraph owns the point and the link does not fire, with or without the extra line-height.

The nuance that makes this worth knowing: line-height still helps, just not the way the advice implies. It does not grow the target. It grows the distance between the centres of adjacent lines — and centre distance is exactly what the Spacing exception measures. So line-height is a spacing fix wearing a size-fix costume.

Result 3: the pseudo-element is the only fix that changes nothing else

Look at the last row of that table. ::after with inset: -8px leaves the layout box at 50.2 × 21 — the text does not move, the paragraph does not reflow, the visual design is pixel-identical — and yet clicks that land 16px above or below the visible link activate it.

.inline-link { position: relative; } .inline-link::after { content: ""; position: absolute; inset: -8px; /* grows the hit region, not the box */ } 

Add pointer-events: none to that pseudo-element and the benefit disappears completely — measured hit area returns to 60.2 × 21. The overlay has to be hit-testable; it is the pseudo-element being struck that counts, not the anchor's box.

This is the fix for links inside running prose, where padding would break the line. It is also the one that automated size audits miss, because getBoundingClientRect() still reports 21px.

Result 4: the vertical stack is where real menus fail

Horizontal rows of words almost always pass, because the links are wide and their centres are far apart. Stacked menus and footer lists are the failure case. Five 13px links, line-height: 1.2, measured line box 15.594px, in a flex column at increasing gap:

gap Pair centre distances Links failing AA
0 15.72 – 17.41 5 of 5
4 19.69 – 21.07 5 of 5
6 21.68 – 22.94 5 of 5
8 23.74 – 24.86 2 of 5
10 25.67 – 26.74 0
12 27.66 – 28.66 0

The vertical delta is just line box + gap, so 15.6 + 8 = 23.6 — under 24. What rescues three of the five pairs is the horizontal offset: "Home" and "Services" have different widths, so their centres differ in x, and the Euclidean distance is larger than the vertical one. At gap: 8 the four pairs land between 23.74 and 24.86, straddling the pass line, and two links still fail.

I re-ran each variant across 10 independent page loads. The failing counts did not change between runs, so the spread above is variation between pairs on one page, not measurement noise. gap: 8 is not a passing value that sometimes reads as failing; it is a value that sits on the boundary and fails the tightest pair.

The padding route is cleaner and has a sharper cliff. Same list, display: block, padding instead of gap:

Padding Link height Centre distance Links failing AA
4px 0 23.594 23.594 5 of 5
6px 0 27.594 27.594 0
8px 0 31.594 31.594 0
12px 0 39.594 39.594 0

padding: 4px 0 on 13px type gives you a target 0.406 CSS pixels short of the minimum. It fails the size floor and it fails the spacing test with the same number, so the exception cannot save it. Six pixels clears both at once.

Result 5: auditing our own site

I ran the same measurement against the live Design Den Solutions site at 390 × 844.

Page Targets measured Under 24px AA failures AAA failures
home 40 0 0 0
web-development 34 2 0 0
contact 33 2 0 0

Median target area 16,104 px². The mobile nav links render at 366 × 44, the social and menu buttons at 48 × 48. Every target that fell under 24px was rescued by the Spacing exception:

  • "See all web projects →" 168.3 × 21, nearest centre 172px
  • "mobile app development" 187.8 × 19, nearest 219.9px
  • footer "info@designdensolutions.com" 219.4 × 19, nearest 57.4px
  • footer "+91 98721 90965" 116.6 × 19, nearest 57.4px

I am not going to present that as a clean bill of health. Four targets pass only because nothing else happens to be near them, and the audit does not evaluate the Inline exception — two of those four sit inside running text, where the Inline exception is the correct defence and the spacing arithmetic is beside the point. A passing score from an incomplete evaluator is a starting position, not a conclusion.

The snippet

This is what I ran. Paste it into DevTools on any page.

const tapAudit = (min = 24) => { const sel = 'a[href],button,input:not([type=hidden]),select,textarea,[role=button],[role=link],summary'; const sx = scrollX, sy = scrollY; const boxes = [...document.querySelectorAll(sel)] .map(el => { const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || Number(cs.opacity) === 0) return null; const r = el.getBoundingClientRect(); if (r.width < 1 || r.height < 1) return null; return { el, cx: r.x + sx + r.width / 2, cy: r.y + sy + r.height / 2, w: r.width, h: r.height }; }) .filter(Boolean); return boxes .map(b => { const others = boxes.filter(o => o !== b); const nearest = others.length ? Math.min(...others.map(o => Math.hypot(o.cx - b.cx, o.cy - b.cy))) : Infinity; const tooSmall = b.w < min || b.h < min; return { target: (b.el.innerText || b.el.getAttribute('aria-label') || b.el.tagName).trim().slice(0, 40), size: `${b.w.toFixed(1)} x ${b.h.toFixed(1)}`, tooSmall, nearestCentre: Number.isFinite(nearest) ? +nearest.toFixed(1) : null, FAILS: tooSmall && nearest < min }; }) .filter(r => r.FAILS); }; console.table(tapAudit(24)); 

It returns only targets that fail the size floor and the spacing test, which is the correct conjunction. Three things it will get wrong, on purpose:

  1. It measures the whole document, not the visible viewport — content below the fold is included, which is what you want for an audit but is not what "what the user sees" means.
  2. It skips anything hidden with display: none, visibility: hidden or opacity: 0, but an off-canvas panel that is merely translated or clipped still reports its targets.
  3. It ignores the Inline, Equivalent, User agent control and Essential exceptions, so a reported failure may still conform.

Two bugs I hit writing it, both of which produce confident wrong answers:

  1. Rounding before the distance maths. I rounded widths to one decimal, then computed centres from the rounded values. That moved a 23.74px pair across the line and reported three failing links where the answer is two. Round when you print, never when you compare.
  2. Treating the spacing test as vertical. Math.hypot over both axes is what the circles do. Using only dy fails a horizontal row of wide links that actually passes.

What I'd do with this

  • Do not size to 24. The floor is where the argument starts, not where it ends. Every value in the tables above that sits near 24 needed a second measurement to interpret.
  • padding: 6px 0 on 13px stacked links is the smallest change that clears both halves of the test outright. Four pixels does not, by 0.406px.
  • Use the ::after overlay for links inside prose, where padding would reflow the line.
  • Test at the CSS pixel level, not the visual level. Nothing in the 50.2 × 21 result is visible in a screenshot.
  • Report failures as pairs, not elements. "Two of five links fail" is a different fix from "the menu fails".

Limitations

One machine, one Chrome build (157), one viewport (390 × 844 CSS px). The lab pages are synthetic and the live audit is of the site as served today, which is not the same as the working copy in the repository. elementFromPoint is viewport-bound, so hit-area probing only happens in the fixed-layout pages. The Fitts' law index of difficulty — ID = log2(1 + D/W), at a 120px travel distance giving 3.09 bits for a 16px target against 1.90 for a 44px target, i.e. 1.63× the pointing difficulty — is a geometric model of task difficulty. It is not a human-subject study, and I ran no user testing and claim no mistap rate. Anyone telling you a percentage of taps that miss is making a different claim than the one the data supports.

Prepared with AI assistance and reviewed for accuracy by Design Den Solutions. The cover image is a conceptual AI illustration, not a screenshot. Measurements are this article's own, from the harness described above.

Design Den Solutions builds mobile-first, responsive sites and does the UI/UX and wireframing before the code, including cross-device testing — see what that covers on the web development page.