A type scale and its rhythm
beginner·8 min·Markdown for agents
What this lesson teaches
Set every text size from a short scale named by role, give each step the right line-height and tracking, and load fonts so nothing fakes or jumps.
Pick a ratio below. Every size on the page recalculates from one base number, and each step already knows its line spacing and letter spacing.
Ratio
That list is a . It is the single most effective thing you can do for text on a screen, and the thing generated UIs skip most often.
Why a short list beats free choice
When every component picks its own size (15px here, 17px there, 13.5px in a badge), the page feels cramped in one spot and airy in the next, and nobody can say why. The cause is one-off sizes.
A scale is a short, closed list. A new size needs a reason. Two common ways to build one:
- A ratio: each step is the last one times 1.2 or 1.25, starting from a 16px body.
- A stock scale, like Tailwind's
text-sm,text-base,text-lg.
Both are fine. What matters more is the names. On a team, name steps by role, text-caption, text-card-title, text-page-title, because a role polices itself in a way text-lg can't. Nobody puts a page title in text-caption.
Sizes go in , not px, so a reader who bumped up their browser's text size gets bigger text. Pixels are for hairlines, not words.
Leading follows the role
is the space between lines, set in CSS as line-height.
- Body text: 1.5 to 1.6. Paragraphs need room for the eye to find the next line.
- Headings and display: about 1.1. Big letters carry their own space; 1.5 on a headline looks like double spacing.
And always unitless. Push the slider below as if the reader zoomed in.
line-height: 24pxLeading is the space between lines. Set it as a ratio of the font size and it grows with the text, whatever size the reader picks.
line-height: 1.5Leading is the space between lines. Set it as a ratio of the font size and it grows with the text, whatever size the reader picks.
line-height: 24px looks identical to 1.5 at 16px, then breaks on the first size change or zoom. Space between paragraphs is about one line-height, not some arbitrary pixel number.
Tracking follows the size
Fonts are spaced for body sizes. At headline sizes that spacing looks loose; on tiny labels it looks cramped. So:
| Size | Letter-spacing |
|---|---|
| Tiny uppercase labels | about +0.06em |
| Body, 14–18px | 0 |
| Headings, 20–48px | −0.01 to −0.03em |
| Display, 64px+ | −0.04em or so |
Track in em, so it scales with the text. You'll play with this properly in the next lesson.
Files that load cleanly
Three rules from Emil Kowalski's practice stop the most common font bugs:
- Ship and nothing else. It's compressed and universal. A
.ttfinpublic/fontsis a bug. Load only the weights you use. font-synthesis: noneat the root. Ask for a bold you never loaded and the browser fakes one by smearing the letters (). Turn that off so a missing file looks missing.- Use the high-level property, not the raw code.
font-weight: 620, notfont-variation-settings: "wght" 620;font-variant-numeric: tabular-nums, not"tnum" 1. Properties still do something sensible when a fallback font loads; raw codes silently do nothing.
:root {
font-synthesis: none;
-webkit-font-smoothing: antialiased;
}Light weights are size-bound: a 300 weight that looks elegant at 32px is unreadable at 11px.
Never change weight on hover or select
One last trap, and it's everywhere. Click through these tabs in both modes.
Click through the tabs: the words to the right jump each time.
Bold letters are wider, so the selected word grows and shoves its neighbours. Hold the weight constant and show state with color (or an underline). The same goes for hover: a menu item that turns bold under the pointer makes the whole row twitch.
Pairing, briefly
Two typefaces cover most products; three is the ceiling. Pair across categories, a serif for display over a sans for text, never two similar sans faces, which read as an accident. And because two families at the same font-size rarely look the same size (the decides), re-tune sizes whenever you swap a face.
Set up our typography: - A closed type scale of 5–6 steps in rem, named by role (text-caption, text-body, text-card-title, text-section, text-page-title). Replace every one-off font-size with a step. - Unitless line-height: 1.5–1.6 for body, ~1.1 for headings. Paragraph spacing ≈ one line-height. - Tracking in em: 0 for body, -0.01 to -0.03em for headings, +0.06em for small uppercase labels. - woff2 only, only the weights we use; font-synthesis: none on :root; high-level properties instead of font-variation-settings. - No font-weight change on hover or selected states; use color.
Quick check
A heading is set with line-height: 32px. What's wrong with it?
In the skill
The nodes your agent reads when it does this work.
- MOC — TypographyHumanizing text on screen. Font choice, line length, tracking.
typography/MOC-typography - Type scale and rhythmEvery size comes from a closed scale named by role. Leading unitless — ~1.1 display, 1.5–1.6 body. Tracking follows size. woff2 only, font-synthesis none, high-level properties over raw tags, tabular figures on anything that updates.
typography/type-scale-and-rhythm
Connected lessons
What this lesson builds on and leads to, across every course.