Skip to main content

Building a Modular Type Scale: Stop Guessing Font Sizes

A practical guide to modular type scales — why a 1.25 or 1.333 ratio beats hand-picked font sizes, how the base × ratio^step formula works, and how to ship it as CSS variables and rem.

Published By 李雷
#typography #css #design-systems #frontend

Building a Modular Type Scale: Stop Guessing Font Sizes

Open a stylesheet you inherited and you will usually find font sizes like 13, 15, 19, 22, and 29 pixels scattered across a dozen files. Each one made sense to whoever typed it that afternoon. Together they make no sense at all — there is no system, no relationship, no reason a heading is 22 instead of 24. A modular type scale fixes this by replacing every gut-feel number with a single rule, and the math behind it is small enough to hold in your head.

What a modular type scale actually is

A modular scale is a set of font sizes where each step is the previous one multiplied by a fixed ratio. That is the whole idea. The formula is:

size = base × ratio^step

Pick a base (say 16px) and a ratio (say 1.25), and the steps going up are 16, 20, 25, 31.25, 39.06… and going down are 12.8, 10.24… Every jump is the same proportion, so the hierarchy reads as intentional rather than random — the same reason a musical scale sounds right. That musical analogy is not decorative: the common ratios carry note-interval names. 1.25 is the Major Third, 1.333 is the Perfect Fourth, and 1.618 is the Golden Ratio.

This is why "just type a number" is the wrong instinct. A hand-picked 22px heading has no defined relationship to your 16px body or your 28px hero. A scale gives every size a slot, and the next size you need is already decided for you.

Why use a ratio instead of picking sizes

There are three concrete payoffs, and none of them are aesthetic hand-waving.

First, consistency that survives edits. When a stakeholder asks to "make everything a bit bigger," you change one number — the base — and re-export. Every size moves proportionally. Hand-tuned scales require you to re-eyeball all twelve values and you will get it slightly wrong.

Second, a defined slot for the next addition. Add a new component six months from now and you do not invent a font size. You reach for the next step that already exists. The scale absorbs growth instead of accreting noise.

Third, it scales the small sizes too. Most people remember to define the big headings and forget captions, legal text, and overlines. The same formula runs downward (negative steps), so the tiny sizes are on the system instead of being one-off guesses at the bottom of the file.

A note on choosing the ratio, since this trips people up. Tighter ratios (1.067–1.2) keep headings and body close, which suits dense dashboards and documentation where vertical space is scarce. Mid ratios (1.25–1.333) are the safe default for marketing pages and blogs — enough contrast to read the hierarchy at a glance. Large ratios (1.5–1.618) create dramatic editorial headlines but get unwieldy past about four steps up: 1.618 at +6 steps from a 16px base is over 280px, which is fine for a poster and unusable as a UI scale. (The naming convention and ratio values come from Tim Brown's Modularscale work, which popularized musical ratios for web typography.)

A real example: base 16, Major Third

Let me run the numbers so you can see the scale fall out of the formula. Base 16px, ratio 1.25 (Major Third), with the Type Scale Generator doing the arithmetic:

| Step | Calculation | Size (px) | rem (16px root) | |------|-------------|-----------|-----------------| | +5 | 16 × 1.25⁵ | 48.83 | 3.05rem | | +4 | 16 × 1.25⁴ | 39.06 | 2.44rem | | +3 | 16 × 1.25³ | 31.25 | 1.95rem | | +2 | 16 × 1.25² | 25.00 | 1.56rem | | +1 | 16 × 1.25¹ | 20.00 | 1.25rem | | 0 | 16 × 1.25⁰ | 16.00 | 1.00rem | | −1 | 16 × 1.25⁻¹ | 12.80 | 0.80rem | | −2 | 16 × 1.25⁻² | 10.24 | 0.64rem |

Look at the relationships. The +2 step lands on exactly 25px, the +4 on a clean 39px hero. Body is 1rem, a caption is 0.8rem. You did not choose any of these — the ratio did. If 48.83 feels too large for your largest heading, you do not edit that one cell (that breaks the entire premise). You lower the ratio or reduce the steps-up count, and the whole scale re-derives.

Shipping it: CSS variables and rem

A scale you cannot put in code is just a pretty table. Export it as CSS custom properties and the whole thing becomes referenceable tokens:

:root {
  --text-base: 1rem;     /* 16px */
  --text-lg:   1.25rem;  /* 20px */
  --text-xl:   1.5625rem;/* 25px */
  --text-2xl:  1.953rem; /* 31.25px */
  --text-3xl:  2.441rem; /* 39px */
  --leading-base: 1.5;
  --leading-3xl:  1.2;
}

h1 { font-size: var(--text-3xl); line-height: var(--leading-3xl); }
.caption { font-size: var(--text-sm); }

Prefer rem over px. A rem size scales with the reader's browser font-size setting, so someone who bumps their default to 20px gets your entire scale proportionally larger — an accessibility win you get for free. A px scale is locked and ignores that preference entirely. Export px only when you have a concrete reason: some email clients, canvas rendering, or matching a px-based design spec.

One trap worth naming: rem depends on the root font-size. If your CSS sets html { font-size: 62.5% } (the 10px-root trick, so 1rem = 10px) but you generate the scale against the default 16px root, every rem value will be 1.6× too small in the browser. Set the root to match your actual setup before copying rem values. If you are converting individual values by hand instead, a dedicated px to rem converter keeps the root in sync.

How I actually use this on a project

When I start a design system, the first thing I lock down is the scale, before a single component exists. I set the base to 16px, pick Major Third for a balanced UI feel, set steps up to six (text-base through a 6xl hero) and steps down to two (caption plus fine print), and leave line-height on "tighten with size" so headings firm up automatically while body text stays open. Then I export the Tailwind theme.fontSize block and paste it straight into the config. From that point on, nobody on the team writes text-[27px] — they write text-2xl, and there is no rounding argument later because every value came from one formula instead of the Figma inspector. The half hour it takes up front saves an afternoon of "is this 22 or 24?" every quarter after.

The short version

Stop typing font sizes one at a time. Pick a base, pick a ratio, and let base × ratio^step decide the rest. Export it as rem-based CSS variables so it respects user zoom, keep the root font-size honest, and you get a hierarchy that holds together today and absorbs whatever you add next month.


Made by Toolora · Updated 2026-06-13