# Type scales that survive real content > A ratio gives you the sizes, but only real content tells you whether they work. Test your scale against long German words and columns of numbers before you ship it. - Source: https://flashdsm.com/blog/type-scales-for-real-content - Published: 2026-09-11 - Author: Flash DSM Team - Track: Fundamentals - Reading time: 6 min read --- ## Start with a ratio, not a list Most type scales start life as a list someone typed: 12, 14, 16, 18, 20, 24, 30, 36. It holds up until the fourth heading level, when nobody can say why 30 and not 32. A modular scale swaps the list for two decisions: a base size and a ratio. Each step is the one below it multiplied by the ratio. Here are the four ratios people actually use, from a 16px base. Step 0 is body text. Values are rounded to the nearest pixel. | Step | 1.125 | 1.2 | 1.25 | 1.333 | |---|---|---|---|---| | -2 | 13 | 11 | 10 | 9 | | -1 | 14 | 13 | 13 | 12 | | 0 (body) | 16 | 16 | 16 | 16 | | 1 | 18 | 19 | 20 | 21 | | 2 | 20 | 23 | 25 | 28 | | 3 | 23 | 28 | 31 | 38 | | 4 | 26 | 33 | 39 | 51 | | 5 | 29 | 40 | 49 | 67 | | 6 | 32 | 48 | 61 | 90 | Read it by column. 1.125 barely moves: six steps up gets you 32px, a modest heading. That suits dense product UI, where you need many levels and have little room. 1.333 reaches 90px in the same six steps, which is a marketing page, not a settings screen. Most product systems land on 1.2 or 1.25. Check the small end too. On 1.333, step -2 is 9px, which nobody can read. Most systems stop one step below body, or two on a tight ratio. Round to whole pixels; nobody can see 39.06px. Some teams snap to even numbers, so 1.25 becomes 12, 16, 20, 24, 32, 40, 48. That's no longer strictly modular, and that's fine. The ratio is how you make the decision once, not a law. ## Line height shrinks as size grows Body text needs room between lines so the eye can find the start of the next one: about 1.5 at 16px. Set a 49px headline at 1.5 and it reads as three separate lines that happen to sit near each other. As size goes up, line height as a ratio comes down. Here is the 1.25 scale with line heights that follow that rule: | Role | Size | Line height | Computed | |---|---|---|---| | label | 13px | 1.4 | 18px | | body | 16px | 1.5 | 24px | | heading-sm | 20px | 1.4 | 28px | | heading-md | 25px | 1.28 | 32px | | heading-lg | 31px | 1.2 | 37px | | display-sm | 39px | 1.15 | 45px | | display | 49px | 1.1 | 54px | Write line height unitless in CSS (`line-height: 1.5`, not `24px`). A unitless value is inherited as a ratio, so a child with a different font size gets a line height that fits it. A pixel value is inherited as pixels and goes wrong the moment someone nests a bigger heading inside. Test display line heights on a headline that wraps. At 1.1, descenders on the first line shouldn't touch ascenders on the second. If they do, go up to 1.15. `text-wrap: balance` on headings stops the lonely last word, and it costs one line of CSS. ## Keep lines 60-75 characters Measure is line length. Past about 75 characters the eye loses its place on the way back to the start of the next line. Much under 45 and the reader is jumping lines every couple of words. For running text, 60-75 characters is the comfortable band. WCAG's AAA criterion 1.4.8 caps it at 80. ```css :root { --measure-body: 65ch; } .prose { max-width: var(--measure-body); } ``` `ch` is the width of the zero glyph in the current font, so `65ch` is an estimate, not a count. Paste a real paragraph and count one line before you trust it. Make measure a type token, not a layout width. It depends on the font and size, not on the grid. ## Three weights are enough Regular (400) for text, medium or semibold (500 or 600) for labels and UI emphasis, bold (700) for headings. Most systems never need a fourth. Each extra weight is another file to load and another option to pick wrongly. A codebase with 300, 400, 500, 600, 700 and 800 in use got there one pull request at a time. Ask for a weight you didn't load and the browser fakes it by thickening the regular, which looks muddy. `font-synthesis: none` turns that off, so the missing weight renders as regular and someone notices in review. Weight and size both signal hierarchy. A heading three steps up the scale may not need bold as well. ## Fluid type with clamp() Instead of jumping at breakpoints, a size can grow with the viewport between a floor and a ceiling. This heading is 32px on a 360px-wide phone and 48px at 1280px, growing in a straight line between: ```css :root { /* 32px at a 360px viewport, 48px at 1280px */ --font-heading-lg-size: clamp(2rem, 1.6087rem + 1.7391vw, 3rem); /* 16px at 360px, 18px at 1280px */ --font-body-size: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem); } ``` The maths, so you can make your own. The slope is (max size - min size) / (max viewport - min viewport): (48 - 32) / (1280 - 360) = 0.017391, which is 1.7391vw. The intercept is min size - slope × min viewport: 32 - 0.017391 × 360 = 25.739px, or 1.6087rem at a 16px root. Check both ends: at 360px, 25.739 + 6.261 = 32. At 1280px, 25.739 + 22.261 = 48. > [!WARNING] > Keep a rem term in the middle value. Text sized in pure `vw` doesn't grow when someone zooms the browser, which fails WCAG 1.4.4 (resize text to 200%). A rule of thumb: keep the ceiling under about 2.5 times the floor so zoom still has room to work. Body text barely needs to be fluid; 16 to 18px is plenty. Save the big ranges for display and headings. ## Name type tokens by role A type token is a bundle, not one number: size, line height, weight and family travel together. Name the bundle after what the text is for. | Token | Used for | On the 1.25 scale | |---|---|---| | display | hero headlines, marketing | 49/54, 700 | | heading (sm, md, lg) | page, section and card titles | 20/28 to 31/37, 700 | | body | paragraphs, table cells, form values | 16/24, 400 | | label | buttons, form labels, tabs, captions | 13/18, 500 | | code | inline code, IDs, token names | mono, one step below body | ```css :root { --font-family-sans: "Source Sans 3", system-ui, sans-serif; --font-family-mono: "JetBrains Mono", ui-monospace, monospace; --font-body-size: 1rem; --font-body-line-height: 1.5; --font-body-weight: 400; --font-heading-md-size: 1.5625rem; /* 25px */ --font-heading-md-line-height: 1.28; --font-heading-md-weight: 700; --font-label-size: 0.8125rem; /* 13px */ --font-label-line-height: 1.4; --font-label-weight: 500; } ``` Never name a type token after its size. `--font-heading-md` can move from 25px to 24px next quarter and every usage stays correct. `--font-25` becomes a lie. Don't name them `h1` to `h6` either: the document outline and the visual size are separate decisions, and a sidebar `h2` is often label-sized. More on this in [naming design tokens](/blog/naming-design-tokens). ## Test with the content you'll get Specimen text lies. "The quick brown fox" has short words, no numbers and never wraps badly. Before you sign off a scale, set it with this: - **Long words.** German will hand you *Rechtsschutzversicherungsgesellschaften* (39 letters) in a 320px card. Headings break first, because display sizes fit fewer characters per line. - **Numbers in columns.** Prices, dates and counts in a table. Proportional figures make columns wobble because a 1 is narrower than an 8. `font-variant-numeric: tabular-nums` lines them up, if the font has tabular figures. Many display faces don't, so check. - **Your longest real title.** Not "Heading". The actual longest page title in the product, at the smallest viewport your fluid scale supports. - **Bold inside body text.** A bold word mid-sentence shouldn't change the line height or push the line wider than the measure. ```css .content:lang(de) { hyphens: auto; } .content { overflow-wrap: break-word; } .data-table td { font-variant-numeric: tabular-nums; } ``` `hyphens: auto` only works when the browser knows the language, so the `lang` attribute has to be right. `overflow-wrap: break-word` is the safety net for when it isn't. > [!TIP] > Keep a page in your docs that renders every type token with real product copy: the longest title, a German paragraph, a table of prices. Look at it every time the scale changes. In Flash, type lives under [[Foundations > Typography]]. You can see the "Fonts in use", pull in any Google font by name or upload a custom font, and edit sizes, weights and line heights as tokens. Ask Flash ({{⌘K}}) has quick actions for the common moves, like "Shuffle font pairing", "Bigger & bolder" and "Editorial". Each one comes back as a patch card you **Apply** or **Discard**, so you can try a direction without losing the scale you had.