A visual audit of CogniPrep reported this: the back link at the top of every blog, guide and employer article is 20 pixels tall.
The markup looked fine. That is the point.
<Link
href="/blogs"
className="text-muted-foreground hover:text-foreground mb-10 inline-flex items-center text-sm font-medium transition-colors"
>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} className="mr-2" />
Back to Blogs
</Link>
An inline flex box around a 16px icon and a 14px label is 20 pixels tall, because that is what the content measures. There is nothing wrong with it visually. It is just a thin horizontal strip to hit with a thumb, at the very top of the page, where a thumb is least accurate.
Three numbers, and only one of them is a requirement
Before fixing it I had to settle which target size we were aiming for, because there are three common answers and people quote them interchangeably:
- WCAG 2.2, 2.5.8 Target Size (Minimum), level AA: 24 by 24 CSS pixels. This is the one that is actually a conformance requirement for most teams, and it comes with exceptions.
- WCAG 2.1, 2.5.5 Target Size (Enhanced), level AAA: 44 by 44 CSS pixels. Comfort, not conformance.
- Platform guidance: 44pt on iOS, 48dp on Android. Where the number 44 in everybody's head comes from.
The exception in 2.5.8 is the part that changes how you measure. A target smaller than 24 pixels still passes if a 24 pixel diameter circle centred on it does not intersect the circle of any other target. In practice that is a statement about spacing: centre to centre distance of at least 24 pixels. There are also exceptions for links inline in a sentence, for user agent controls, and where the size is essential.
So "this link is 18 pixels tall" is not a finding on its own. Height without spacing cannot tell you whether something passes.
What we changed
The back link went to 44 pixels in all three article shells, with one extra edit that matters more than the first:
className="text-muted-foreground hover:text-foreground mb-4 inline-flex min-h-11 items-center text-sm font-medium transition-colors"
min-h-11 is 44 pixels. The text size did not change, the icon did not change, nothing moved on the page except the invisible box around it. And mb-10 became mb-4, because adding 24 pixels of hit area to the bottom of an element that already had 40 pixels of margin below it opens a visible hole under the heading. If you grow a target, take the growth back out of the adjacent margin or your layout gains a gap nobody asked for.
The same pass raised the phone logo link and the search button in the app header to 44, and the report download button to 40 by 40 on phones.
What we deliberately left small
Two decisions are more interesting than the fix.
Breadcrumb links on 52 hub pages are 18 pixels tall, and they stay that way. They pass 2.5.8 through the spacing exception, the markup is inline in all 52 pages rather than in a shared component, and a 52 file edit in a release branch for a criterion that already passes is how you get a regression instead of an improvement. It is written down as a separate site-wide pass if we decide we want one.
A small label above each card went to 32 pixels, not 44. On the format pages, each card carries a provider name above it in 12px uppercase. At 44 pixels tall that label stops reading as a label on the card and starts reading as its own row, so it got min-h-8 instead: comfortably over the 24 pixel minimum, visually still attached to what it belongs to.
The point of both is that target size is a layout decision with a floor, not a number to maximise.
Measure both numbers at once
Here is the snippet I use now. For every link on the page it reports the height and the distance to the nearest other link centre, which is enough to apply 2.5.8 by eye:
const links = [...document.querySelectorAll('a, button')]
.map((el) => {
const b = el.getBoundingClientRect();
return { text: el.innerText.trim().slice(0, 24), h: Math.round(b.height), cx: b.left + b.width / 2, cy: b.top + b.height / 2 };
})
.filter((l) => l.h > 0);
links
.map((l) => {
const nearest = Math.min(...links.filter((o) => o !== l).map((o) => Math.hypot(l.cx - o.cx, l.cy - o.cy)));
return { text: l.text, height: l.h, nearestCentre: Math.round(nearest) };
})
.sort((a, b) => a.height - b.height)
.slice(0, 10);
Sort by height, read the nearest centre column, and any row with height under 24 and nearest centre under 24 is a real failure. Rows with a small height and a large spacing number are a judgement call about comfort, which is a different conversation and a cheaper one.
See it on the live pages
With a 390 wide viewport:
- A blog article or a guide. The back link at the top measures 44 by 115 and 44 by 210. Before this pass both were 20 tall.
- A provider hub. The Home breadcrumb is 18 pixels tall. Run the snippet: its nearest neighbour centre is far enough away that the spacing exception applies.
- The footer on any page, for example a format page. The legal links are the smallest targets on the whole site at 16 pixels tall, and the nearest centre to centre distance is about 106 pixels. Passing, and a good illustration of why nobody should fix them on the strength of the height alone.
The thing I will do differently
The audit found this because a human looked at a page and thought the strip at the top looked thin. That works, and it does not scale to 52 hubs and hundreds of articles.
Running the snippet above over a page takes a second and produces a sortable table. Running it over a route list in a headless browser turns "is our site thumb friendly" into a report instead of an opinion. That is the next pass, and this time the output will include both columns.
Top comments (0)