The CSS that makes type look designed
Eight properties that separate a page somebody set from a page that used the defaults. Fluid sizes, optical tracking, tabular figures, and the two lines that fix every table of numbers.
Most pages are set by accident. The font is chosen, the sizes come from a scale somebody copied, and every other typographic decision is whatever the browser does when you do not tell it anything.
These are the properties that are actually worth setting. Each one is a real decision with a visible result, and together they are most of the difference between a page that looks styled and a page that looks set.
1. Sizes that scale with the page
Media-query breakpoints for type give you three fixed sizes and two sudden jumps. clamp() gives you a line that grows with the viewport and stops at both ends.
h1 {
font-size: clamp(2rem, 1.2rem + 3.6vw, 4.5rem);
}
Read it as: never below 2rem, never above 4.5rem, and in between it grows with the window. The middle value has a rem component on purpose — a pure vw value does not scale when the user changes their browser's font size, which breaks zoom for the people who need it.
max-width, not with the font size.
2. Tracking that follows the size
Letter spacing is a decision about a size, and a typeface is spaced for one. Large type is set too loose by default; small type and uppercase are set too tight.
h1, h2 { letter-spacing: -0.02em; } /* large: tighten */
h3 { letter-spacing: -0.01em; }
.label { letter-spacing: 0.08em; } /* small caps, all caps: open up */
Use em, never px — tracking has to scale with the size it is correcting.
3. A measure, always
The single most effective typographic property on the web:
p { max-width: 68ch; }
ch is the width of a 0 in the current font, so the value tracks the font and the size. Between 45 and 75 characters is where a line stops being tiring, and a full-width paragraph on a 27-inch monitor is 200 characters.
4. Figures that line up
Proportional figures — where 1 is narrower than 8 — are right in a sentence and wrong in a column. Two lines fix every table of numbers, every price list and every timer:
.price, table td {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
Now the digits all occupy the same width, columns align, and a counting clock stops shuffling the words beside it.
While you are there: slashed-zero for anything a person will read aloud or type back, and oldstyle-nums for figures inside running prose, if the face has them.
5. Hyphenation and the ragged edge
p {
hyphens: auto;
text-wrap: pretty;
}
hyphens: auto needs lang on the html element to know which dictionary to use. Without it, nothing happens and everybody concludes the property is broken.
text-wrap: pretty asks the browser to avoid a last line with a single word on it. text-wrap: balance is the one for headings — it evens out the lines of a two- or three-line title instead of leaving one word stranded.
h1, h2, h3, blockquote { text-wrap: balance; }
6. Real quotation marks and dashes
blockquote { quotes: '\201C' '\201D' '\2018' '\2019'; }
And in the copy itself: — for an em dash, – for a range, ' for an apostrophe. A straight " is a typewriter mark for a measurement in feet. Every text editor will do this for you and almost nobody turns it on.
7. Rendering hints, used sparingly
body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
Both of these are cargo-culted onto every project and both have costs.
-webkit-font-smoothing: antialiased makes text thinner on macOS. On a dark background that is often an improvement; on a light one it can make body text look weak. Decide per design, do not paste it in by default.
text-rendering: optimizeLegibility turns on kerning and ligatures — and on a long page it has a measurable layout cost. Kerning is on by default in every current browser anyway, so the property mostly buys you the ligatures you may not want.
8. Stop the swap from moving the page
The jump when a web font loads is not caused by the swap, it is caused by the fallback having different metrics. Give the fallback the real font's proportions:
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body { font-family: 'Your Font', 'Fallback', sans-serif; }
Get those numbers right and the swap becomes invisible, which is a better outcome than any font-display value on its own.
Putting it together
:root {
--display: 'Typefound Cosmic', Georgia, serif;
--text: Inter, -apple-system, 'Segoe UI', system-ui, sans-serif;
}
html { -webkit-text-size-adjust: 100%; }
body {
font-family: var(--text);
font-size: 1rem;
line-height: 1.65;
text-rendering: optimizeSpeed;
}
h1, h2, h3 {
font-family: var(--display);
line-height: 1.1;
letter-spacing: -0.02em;
text-wrap: balance;
}
h1 { font-size: clamp(2rem, 1.2rem + 3.6vw, 4.5rem); }
p {
max-width: 68ch;
hyphens: auto;
text-wrap: pretty;
}
table td, .price { font-variant-numeric: tabular-nums; }
Twenty lines. None of it is clever, and a page with it looks set rather than defaulted.
One thing not on the list
font-weight: bold on a face that has only one weight. The browser will not refuse — it invents a bold by smearing the glyph sideways, and on a face with hairlines and sharp terminals the result looks like a printing fault.
If a face has one weight, it has one weight. Emphasis has to come from size, colour, spacing or a second typeface. This applies to most display faces, including all of ours, which is why our poster tool grows the real outline rather than asking for a bold that does not exist.