Skip to content
ExamsLibraryFor agentsConnect your agent

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.

  1. 1Why a short list beats free choice
  2. 2Leading follows the role
  3. 3Tracking follows the size
  4. 4Files that load cleanly
  5. 5Never change weight on hover or select
  6. 6Pairing, briefly

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

text-caption0.8rem · 12.8px · lh 1.55 · 0.01emUpdated 2 minutes ago
text-body1.rem · 16px · lh 1.55 · 0Your report is ready to share with the team.
text-card-title1.25rem · 20px · lh 1.3 · -0.01emQuarterly report
text-section1.956rem · 31.3px · lh 1.1 · -0.02emRecent activity
text-page-title3.05rem · 48.8px · lh 1.1 · -0.02emAnalytics
One base, one ratio, five roles. Change either and the whole page moves together.

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: 24px

Leading 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.5

Leading 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.

Fixed 24px leading stacks lines on top of each other as the text grows. Unitless 1.5 grows with it.

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:

SizeLetter-spacing
Tiny uppercase labelsabout +0.06em
Body, 14–18px0
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:

  1. Ship and nothing else. It's compressed and universal. A .ttf in public/fonts is a bug. Load only the weights you use.
  2. font-synthesis: none at 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.
  3. Use the high-level property, not the raw code. font-weight: 620, not font-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-on-select pushes every label after it sideways. Color doesn't move anything.

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 type· for your agent
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?

What this lesson builds on and leads to, across every course.