Most developers set font sizes by eye. The heading looks small, make it 28px. The caption looks big, make it 13px. It works on one page. By page twenty there are fourteen different sizes in the codebase, nothing lines up, and nobody can say why the site looks slightly amateur.
A type scale replaces that with arithmetic: one base size, one ratio, and every size is the previous one multiplied by the ratio. The sizes relate to each other because they were made from each other. Printers have worked from a fixed set of sizes (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72) for a few hundred years; the web version just makes the ratio explicit.
This post covers which ratio to pick, how to put the scale in CSS so it stays put, and the line-height and letter-spacing that go with it. The Typography Scale Calculator generates the numbers for any base and ratio, so you never have to multiply by 1.25 five times by hand.
The ratio decides how loud the headings are
Start from the body size, almost always 16px, and multiply. The ratio names come from music intervals, which tells you nothing useful; what matters is how big the jumps are.
Minor second, 1.067: 16, 17.07, 18.22, 19.44, 20.75. Barely visible steps. Dense dashboards and data tables, where you need eight levels and no drama.
Major second, 1.125: 16, 18, 20.25, 22.78, 25.63. Quiet hierarchy. Documentation, long reads.
Minor third, 1.2: 16, 19.2, 23.04, 27.65, 33.18. The safe default for a general website. Headings clearly headings, nothing shouting.
Major third, 1.25: 16, 20, 25, 31.25, 39.06. Marketing pages and content sites where the H1 has to carry the page.
Perfect fourth, 1.333: 16, 21.33, 28.43, 37.90, 50.52. Landing pages with one big headline and not much else.
Golden ratio, 1.618: 16, 25.89, 41.89, 67.77. Three steps and you are at poster size. Only for designs with two or three text levels in total.
My own choice is not one of these. ToolForte runs on Tailwind's default sizes (12, 14, 16, 18, 20, 24, 30, 36, 48 and up), which follow no single ratio, and I have not changed them because they have not caused a problem. Pick a ratio when the defaults are giving you inconsistent pages, not because a blog post said to. If you do pick one, the Typography Scale Calculator prints the whole ladder in px and rem.

Put the scale in custom properties or it will not survive the next sprint
A scale that lives in a design document is a scale nobody uses. Put it in CSS custom properties, once, and reference the variables everywhere. Then a developer who needs a size has a list to pick from, and a size that is not on the list is visible in code review.
`css
:root {
--font-size-base: 1rem; / 16px /
--font-size-sm: 0.875rem; / 14px /
--font-size-xs: 0.75rem; / 12px /
--font-size-lg: 1.25rem; / 20px /
--font-size-xl: 1.563rem; / 25px /
--font-size-2xl: 1.953rem; / 31.25px /
--font-size-3xl: 2.441rem; / 39.06px /
--font-size-4xl: 3.052rem; / 48.83px /
}
body { font-size: var(--font-size-base); }
h1 { font-size: var(--font-size-4xl); }
h2 { font-size: var(--font-size-3xl); }
h3 { font-size: var(--font-size-2xl); }
h4 { font-size: var(--font-size-xl); }
h5 { font-size: var(--font-size-lg); }
`
That is a major third: every step is the previous one times 1.25, rounded to three decimals.
The units are rem on purpose. A reader who has set their browser default to 20px because their eyes need it gets the whole site scaled up in proportion. Sizes in px ignore that setting, and it is one of the first things an accessibility audit will write up. The PX to REM Converter does the division when a design file hands you pixels.
To make the scale responsive, change only the base. Every other size follows:
`css
:root {
--font-size-base: 0.9375rem; / 15px on mobile /
}
@media (min-width: 768px) { :root { --font-size-base: 1rem; / 16px on tablet+ / } }
@media (min-width: 1200px) {
:root {
--font-size-base: 1.0625rem; / 17px on large screens /
}
}
`
A scale that lives in a design document is a scale nobody uses.
Line height goes down as font size goes up
Font size without line height is half a decision. Line height (leading, if you learned it from print) is the vertical distance between lines. Too tight and the eye loses the line on the way back. Too loose and a paragraph falls apart into separate sentences.
Body text: 1.5 to 1.7. WCAG's text spacing criterion does not set a minimum for you, but it does require that your layout still works when a reader forces line height to 1.5, so designing at 1.5 or above means nothing breaks when they do.
Headings: 1.1 to 1.3. A 48px heading at 1.6 has a gap between its two lines you could park in.
Small text: 1.4 to 1.6. Captions and labels at 12px need a bit more air because the letterforms are harder to tell apart.
The pattern is that the ratio falls as the size rises. Write it unitless:
`css
body { line-height: 1.6; }
h1, h2, h3 { line-height: 1.2; }
small { line-height: 1.5; }
`
Unitless means each element multiplies its own font size. A line height in px, or even in em on a parent, gets inherited as a fixed value and then a child with a bigger font overlaps itself. This one shows up in bug reports as "the heading looks squashed on mobile", and it is always a line height that was set in pixels somewhere higher in the tree.

Letter spacing: tighten the big text, open up the small caps, leave the body alone
A font's designer set its letter spacing for body sizes. At 48px the same spacing reads loose; at 11px in capitals it reads cramped. Three rules cover nearly every case.
Headings: a touch tighter, minus 0.01em to minus 0.03em. Past that the letters start touching.
`css
h1 {
letter-spacing: -0.02em;
}
`
Body text: leave it. Someone who draws letters for a living already made this decision, and I have never seen a body-text tracking change that improved anything.
Small text and uppercase labels: a little wider, 0.02em to 0.05em. Capitals have no ascenders and descenders to separate them, so they need the room.
`css
.label {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
}
`
Word spacing is only worth touching on justified text with rivers of white space in it, and the better fix there is usually to stop justifying.
A font's designer set its letter spacing for body sizes.
Name the styles, not just the sizes
A list of sizes still leaves every developer choosing weight, line height and colour on the spot. A type system bundles those into named styles, so the choice is "this is a caption" and everything else follows.
For a web application the set is short: two or three display sizes for hero text, H1 to H4, two body sizes, a label style for forms and buttons, a caption style, and a code style. Ten to twelve named styles cover almost any product. If you find yourself at twenty, some of them are the same style with a different name.
Each style carries the full set of properties:
`css
.text-heading-1 {
font-family: var(--font-heading);
font-size: var(--font-size-4xl);
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.02em;
color: var(--color-text-primary);
}
.text-body {
font-family: var(--font-body);
font-size: var(--font-size-base);
font-weight: 400;
line-height: 1.6;
letter-spacing: 0;
color: var(--color-text-secondary);
}
`
Put every style on one page in the app itself, a route only the team knows about, showing each name with real text under it. That page is the specification. When a screen looks off, open the page next to it and the stray 15px heading is obvious in a way it never is in a Figma file.
FAQ
What base size?
16px. It is the browser default, it reads fine on a phone, and every rem calculation starts from a round number. Long-form reading sites go to 18px; I would not go higher without a reason you can name.
px or rem?
rem for anything that is text. A reader who set a 20px default did it because they need it, and px sizes ignore them. px is for things that should not grow with the text: a 1px border, a 24px icon.
How many sizes?
Eight to twelve in the scale. Under six and there is no room for hierarchy. Over fifteen and the team stops being able to tell them apart, which means they stop using them.
Can I use two ratios in one project?
You can, and the result is what you had before you started: sizes that do not relate to each other. If the ratio does not produce a size you need, move the base or change the ratio. Do not bolt on a second one.
### What base size.
CSS Button Generators: What to Check Before You Paste the Output
A button generator gives you the resting state. The four other states, the 44 pixel tap target and the focus ring are on you. The checklist I use before the CSS goes in.
Contrast Ratio: The Accessibility Failure on Four Out of Five Home Pages, and the Five-Second Check
Low-contrast text is the most common WCAG failure there is. What the 4.5:1 and 3:1 thresholds mean, why your eyes are a bad judge, the five places designs fail, and how to build a palette where every pairing already passes.
Mesh Gradients: How to Build One That Does Not Look Like a Screensaver
What a mesh gradient is, how to write one as layered CSS radial-gradients, which generators are worth opening, and the rules that keep text readable on top of it.
How I Use ToolForte on a Normal Working Day
ToolForte is a few hundred small tools that run in your browser, plus an API, an MCP server and workflows for the moments a browser tab is not enough. Here is the site as I actually use it, from a test manager's desk.
