# A type scale and its rhythm

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

Module: Typography · Level: beginner · 8 min · https://school.agents.org.in/design/typography/type-scale

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.

*[Interactive demo: One base, one ratio, five roles. Change either and the whole page moves together.]*

That list is a type scale (A short, fixed list of text sizes a product is allowed to use, often each one a set ratio bigger than the last. Every piece of text picks a step instead of a one-off size). 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`.

> **In plain words.** A type scale is a menu of five or six text sizes. Every piece of text orders from the menu. It makes the page feel consistent even when nobody can say why.

Sizes go in rem (A CSS unit relative to the root font size (usually 16px). Sizes in rem grow when a reader sets a bigger default text size; px sizes ignore them), 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

Leading (The vertical space from one line of text to the next. Body text wants about 1.5 times the font size; big headings about 1.1) 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.

*[Interactive demo: 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:

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

1. **Ship woff2 (The compressed font file format every modern browser reads. It is the only format a website needs to 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 (faux bold (When a page asks for a bold or italic the font didn't load, the browser fakes one by smearing or slanting letters. font-synthesis: none turns that off so the problem is visible)). 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.

```css
: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.

*[Interactive demo: 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 x-height (How tall a font's lowercase letters are. Two fonts at the same font-size can look very different sizes because of it) decides), re-tune sizes whenever you swap a face.

**Set up type (paste into your agent):**

```text
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.
```

**Check yourself:** A heading is set with line-height: 32px. What's wrong with it?

- [ ] Nothing, 32px is a good value
- [x] It's fixed: when the font size changes or the reader zooms, the lines overlap or gape. Use a unitless value like 1.1
- [ ] Headings shouldn't have a line-height
- [ ] It should be in rem

*Why:* Unitless line-height scales with the font size. A fixed pixel value breaks on the first size tweak or zoom.

## In the skill

- https://school.agents.org.in/skill/typography/MOC-typography.md
- https://school.agents.org.in/skill/typography/type-scale-and-rhythm.md
