Some bugs never reach the console, never fail a build, and never get reported by anyone except your own eyes after staring at a screen too long. I hit one of those on a client marketing site: the hero heading felt wrong, and it took me an embarrassing amount of time to name what was actually wrong with it.
The symptom: something is clipped, but only barely
The main heading used text-5xl on mobile and stepped up to text-6xl on larger screens, set in Quicksand. At a glance it looked clean. Zoom in and it was not: the tops of tall letters like d, k and t were shaved off, and the tails of g and y lost their tips. Not blurry, not covered by another element. Cut flat, as if an invisible box shorter than the letters was sitting over them.
What made it worse to diagnose is that the clipping was inconsistent between headings. A word made only of short letters looked perfectly fine, so more than once I closed devtools convinced I was imagining it.
My first suspect was the obvious one: the font. I checked that Quicksand was loading properly, checked font-display, checked whether some fallback was sneaking in. All clean. I swapped temporarily to the system sans and the clipping got smaller but did not disappear. That was the first genuinely useful clue: when changing the font only changes how bad it is instead of fixing it, the problem lives in the box holding the text, not in the letters.
And one place made it dramatically more obvious
The hero also ran a per-character reveal animation. Standard pattern: wrap every character in a span with overflow-hidden, then slide an inner span up from below so each letter appears to rise into view.
There the clipping went from subtle to brutal. Every character now had its own clipping box bound to its own line box, so anything that used to spill out just slightly got trimmed off cleanly. The animation turned out to be my best diagnostic tool, ironically: it converted a vague problem into an unmistakable one.
The root cause: a default line-height that snaps to 1
This is the part that cost me time, because nowhere in the project did I write a single line-height declaration.
Tailwind's text size utilities do not only set font-size. Every size ships with a paired default line-height. For small and mid sizes that pairing is generous and proportional. But from text-5xl upward, the pairing snaps to line-height: 1. Exactly one times the font size, nothing more.
And line-height: 1 is not "just enough". In plenty of typefaces, especially rounded geometric ones like Quicksand, the font's own ascent plus descent metrics add up to more than one em. That means the area the glyphs need in order to be drawn is taller than the line box you gave them. The remainder spills out. If nothing clips, you usually get away with it and merely end up with cramped spacing. But the moment an overflow-hidden appears anywhere in the render path, whether from an animation wrapper, a card, or any container, that spill turns into a haircut.
So the chain was: Tailwind hands big headings a line-height of 1, the font needs more than 1em, and the per-character overflow-hidden wrappers carry out the sentence.
The fastest way to prove it: drop a temporary leading-normal onto the offending heading. If the letters immediately come back whole, you have your cause, and the rest is just picking the right number.
The fix: override the token, not every heading
My first instinct was to slap leading-[1.05] on each heading. That works, but it means every new heading anyone writes afterwards is born with the same bug until somebody remembers to add the class.
In Tailwind v4 those paired line-heights are theme variables, so you can override them once, in one place:
@import "tailwindcss";
@theme {
--text-5xl--line-height: 1.05;
--text-6xl--line-height: 1.05;
}From then on, every text-5xl and text-6xl on the site gets a little breathing room without anyone having to remember anything. The 1.05 is deliberately small. The goal is not to loosen the headings and lose their tight, dense look, only to give ascenders and descenders enough room to stay inside their box.
One place I still set by hand: the per-character wrappers in the reveal component. The token only touches elements that actually carry text-5xl or text-6xl, and here the element doing the clipping is the wrapper span. I set the value explicitly there so the clip box is guaranteed to match the heading, whatever text size the component gets dropped into:
<span className="inline-block overflow-hidden leading-[1.05]">
<span className="inline-block">{char}</span>
</span>Once the clipping wrapper uses the same line-height as the heading it belongs to, the animation runs cleanly and the letters stay whole from the first frame to the last.
What I took away
- If letters are clipped and swapping fonts only changes the severity, stop blaming the font and start suspecting the line box.
- Text size utilities are a two for one deal. You set
font-size, but you silently accept aline-heighttoo, and fromtext-5xlup that value is1. line-height: 1is harmless right up until it meetsoverflow-hidden. Any clipping wrapper turns invisible overflow into visible damage.- Fix it at the token level when the problem is systemic. Patching class by class only postpones the same bug until the next heading gets written.