A 44-pixel tap target on an inline link that wraps
•8 min read
The link measured 297 by 44 pixels and reported a pass. Tapping its centre hit the paragraph behind it. An inline box that wraps is not one rectangle, and the pseudo-element meant to enlarge it was positioned against the same broken geometry.
An inline link that wraps onto a second line is not one rectangle. It is two line boxes with a hole between the end of the first and the start of the second, and getBoundingClientRect reports the union of the two, which includes the hole. A measured target of 297 by 44 pixels therefore passed an automated size check while a tap at the centre of that rectangle landed in the gap and returned the parent paragraph. The pseudo-element added to enlarge the target was absolutely positioned against the same fragmented box, so it did not close the hole either.
297 by 44 pixelsThe union rectangle reported for a wrapped inline link at a 390-pixel viewport. Its centre, its bottom and its right all missed the link.
Why does a wrapped link measure larger than it is?
A wrapped inline link measures larger than it is because the geometry API answers a different question from the one being asked. An inline element that breaks across lines generates one box per line fragment, and getBoundingClientRect returns the smallest rectangle containing all of them. For a link ending near the right edge of one line and continuing near the left edge of the next, that rectangle spans most of the paragraph's width and the full height of two lines, while the link itself occupies two horizontal strips inside it.
An automated target-size check reading that rectangle sees a comfortably large target and passes. A finger aimed at the middle of the same rectangle lands between the two strips. The check and the finger disagree because one is measuring a bounding box and the other is hitting whatever is painted at a point, and only the second one is what a person experiences. Calling elementFromPoint at the centre of the union returns the paragraph, which is the measurement that matters.
Why does an absolutely positioned extender not fix it?
The usual way to enlarge a small control is a pseudo-element centred on it: absolutely positioned, translated back by half its own size, sized to the full width and height of the parent with a minimum of the target floor. On an atomic box that works, and it buys the extra pixels without changing anything drawn. On a fragmented inline box it does not, because the pseudo-element is positioned against the same broken geometry. It centres itself on the union rectangle, which means it centres itself on the hole.
44pxThe hit-area floor below 48rem. It is a hit area rather than a drawn one: a 32px row action that grows to 44px stops being a row action.
The accessibility floor is written by redefining tokens inside media queries. That only keeps working if every Tailwind utility emits a var() reference rather than a compiled copy.
Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
No card to start. Cancel from the settings screen, not from an email.
Measured on a real page at a 390-pixel viewport, the centre, the bottom and the right of that union all missed the link. That is the failure worth remembering: the mechanism designed to guarantee a minimum target had produced a target with a hole in the middle, and every automated check in the pipeline reported it as compliant. A geometry-based fix cannot repair geometry it is measured against.
What actually fixes it?
The fix is to stop the box fragmenting. Setting the link to inline-flex makes it one atomic box, at which point the bounding rectangle and the hit area describe the same thing and the minimum-height rule works as intended. That is also what the element always was conceptually: a paragraph whose entire content is a link is a call to action rather than prose, and a call to action is a button-shaped thing that happens to be an anchor.
A link inside a sentence must not get the same treatment. An atomic box that cannot break across lines is worse than the target it buys, because it either overflows its container or forces an ugly line break in the middle of a paragraph. WCAG 2.5.8 recognises this directly by exempting a target that is constrained by the line-height of surrounding non-target text. The exemption exists because the alternative damages the reading experience for everybody in order to help nobody.
Four link shapes, and why the same remedy is right for only two of them.
Link shape
What the rectangle says
What a tap does
Correct treatment
Inline link inside a sentence
Union of its line fragments
May land in the gap when it wraps
Leave it inline; covered by the line-height exemption
Paragraph whose whole content is a link
Union of its line fragments
Misses the centre once it wraps
inline-flex, so it becomes one atomic box
Link alone in a table cell
The anchor's line box only, not the cell
Misses everywhere except the text itself
Fill the cell and set a minimum block size, so the row grows
Icon button
Its own small box
Hits, but the box is under the floor
A centred pseudo-element extender works, because the box is atomic
How do you decide which links get the treatment?
Selecting the right links is where this gets subtle, and a structural selector alone is not enough. Matching a link that is the only child of a paragraph sounds correct until you remember that the only-child test counts element siblings and ignores text. A paragraph reading "Leave this empty to use the account footer identity", where the last three words are a link, matches that selector: the anchor is the only element in the paragraph even though it is surrounded by prose. Applying the atomic-box rule to it turned the end of a sentence into a tall block inside its own paragraph.
The gate that works pairs the structural test with an explicit declaration. A link a builder has marked as part of the target system has been declared a control, and an unmarked link in prose is left where it belongs. Requiring both the attribute and the only-child position gives a rule that catches the calls to action and leaves the sentences alone, without asking a stylesheet to guess intent from structure it cannot see.
What about a link that is the title of a table row?
A link that is the title of a table row fails for a related but different reason: the cell is large and the anchor is not. An inline anchor is only as tall as its line box, so the rest of the cell belongs to the cell rather than to the link, and a finger aimed at the row's own name lands on the row instead of on the way into it. Measured on a post-listing screen, one such title was 340 by 18 pixels as drawn and 61 by 19 pixels as tapped.
340 by 18 drawn, 61 by 19 tappedA table row-title link. The cell is wide, the anchor's line box is not, so most of the visible row is not part of the target.
Because table rows stack flush against each other, the remedy there is pitch rather than an extender. Making the link fill its cell and setting a minimum block size grows the row if it is shorter than the floor and changes nothing if it is already taller, and every row grows together. An overlapping pseudo-element would instead produce two 44-pixel boxes fighting in any cell holding a link beside a badge, which is a defect rather than a fix.
What should you test instead?
Hit-test points, not bounding boxes. Ask what element is painted at the centre, and repeat at the bottom and the right edge.
Test at a narrow viewport where text actually wraps. A target that passes at desktop width can fail at 390 pixels for purely typographic reasons.
Treat any inline element as suspect until you know whether it fragmented. The union rectangle tells you nothing about the gap inside it.
Check the element returned by the hit test, not just whether something was returned. Getting the parent paragraph back is the failure signature.
Apply the atomic-box rule only to links that were declared controls, and leave links inside sentences inline.
For a link filling a cell or a row, grow the container rather than overlaying a pseudo-element, so neighbouring targets do not overlap.
The first item is the general lesson, and it is the same one that appears whenever a check verifies a declaration rather than an outcome. A rule that says a target must be at least a certain size is only as good as the measurement it trusts, and a bounding rectangle is a declaration about extent rather than an observation about what a finger will hit. That is precisely the distinction that let a hidden element keep its padding while every class-name assertion passed.
Why does my tap target pass an automated check but fail in the hand?
Because the check almost certainly reads a bounding rectangle. For an inline element that wraps, that rectangle is the union of its line fragments and includes the empty space between the end of one line and the start of the next. The rectangle can be large while the target itself is two thin strips. Hit-testing a point tells you what is really there: call elementFromPoint at the centre of the rectangle and see whether the link or its parent comes back.
Should I make every inline link an atomic box?
No. A link inside a sentence should stay inline, because an atomic box cannot break across lines and will either overflow its container or force an awkward break mid-paragraph. WCAG 2.5.8 exempts targets constrained by the line-height of surrounding non-target text for exactly this reason. Reserve the atomic-box treatment for links that are calls to action in their own right, which in practice means links the builder has explicitly marked as controls.
Why does a centred pseudo-element not enlarge a wrapped link?
Because it is positioned against the same fragmented geometry that caused the problem. The pseudo-element centres itself on the containing block of the inline element, which for a wrapped link is the union of its line fragments, so the extender centres itself on the gap between the two lines. The result is a larger rectangle with the same hole in the middle. It works on atomic boxes such as buttons and icons, where the containing box has no gap.
How do I select only the links that need this?
Pair a structural test with an explicit marker. Selecting a link that is the only child of a paragraph is not sufficient on its own, because the only-child test counts element siblings and ignores text nodes, so a link at the end of a sentence matches. Requiring both that position and an attribute the builder applies to declare something a control gives a rule that catches calls to action and leaves prose links alone.
Does the same problem affect links in table cells?
It affects them differently. There the anchor does not usually wrap; it is simply only as tall as its own line box, so most of a tall cell is not part of the target even though it looks like the way into the row. The fix is to make the link fill the cell and set a minimum block size, which grows the row when it is shorter than the floor. Because rows stack flush, this changes pitch rather than overlaying boxes that could collide.
A screen-reader-only caption kept its padding after being hidden, and the invisible node measured 32 by 16 pixels instead of 1 by 1. The cause was not the hiding technique. It was two selectors of equal weight and the order they were imported in.
A div with overflow auto and no tab stop is content a keyboard user cannot reach at all. Making it focusable is half the fix; an unnamed focusable region is announced as a group with no label. What to add, in what order, and why a rewrite is where this gets lost.