Bhinish Dhiman· updated 5 October 2026
How to build a fluid type scale with CSS clamp()
Pick a body size for the narrow and wide ends, multiply up by a ratio for each step, then draw a straight line between the two sizes: slope = size change ÷ viewport change, intercept = narrow size − slope × narrow viewport. For 16px at 360px to 19px at 1440px that gives clamp(1rem, 0.9375rem + 0.2778vw, 1.1875rem). Then check cap-height and characters per line, which clamp() does not.
A fluid type scale is two fixed scales, one for a phone and one for a desktop, joined by a straight line. clamp() is how CSS draws that line and stops it at both ends. The arithmetic takes four steps, and it is worth doing once by hand so the numbers in a generated stylesheet stop being magic.
Two terms first
A type scale is an ordered set of font sizes made by multiplying a body size by a ratio, once per step. At a ratio of 1.25, a 19px body gives 23.75px for the next step up, 29.69px for the one after, and so on.
A fluid size is a size that grows in a straight line with the viewport between two widths, and holds still outside them. In CSS it is written clamp(minimum, preferred, maximum), where the preferred value mixes a fixed length with a viewport unit.
Build the scale with the tool
- Open the Type scale generator and choose the typeface first, from Typeface. Each family in the list shows its x-height, and every measurement further down depends on the family, so picking it last means reading the numbers twice.
- Set Body size, narrow (12 to 24px; it starts at 16) and Body size, wide (14 to 28px; it starts at 19).
- Choose Ratio, wide from seven buttons, from 1.125 (Major second) to 1.618 (Golden ratio). The narrow end uses 1.2, which the panel states under the buttons.
- Set Steps up (1 to 8) and Steps down (0 to 4). The defaults are five and two.
- Set Column width as a percentage of the viewport, and read Body line length, which counts characters per line at 360, 768, 1024, 1280 and 1440px.
- Read each row of the scale: its size at the narrow and wide ends, then its cap-height and x-height in pixels.
- Choose an output: CSS custom properties, Tailwind v4 @theme, SCSS map or Design tokens (W3C DTCG). Press Copy scale in the bar at the bottom. The bar’s Cap at …rem button copies a
max-widththat lands the body at 66 characters per line.
The clamp() arithmetic, worked in full
Take the tool’s defaults for the body step: 16px on a 360px viewport, 19px on a 1440px viewport, and the browser’s default root size of 16px.
Step 1, the slope. How many pixels of font size per pixel of viewport: (19 − 16) ÷ (1440 − 360) = 3 ÷ 1080 = 0.0027778.
Step 2, the slope as a viewport unit. 1vw is 1% of the viewport width, so multiply by 100: 0.0027778 × 100 = 0.2778vw.
Step 3, the intercept. The font size the line would give at a zero-width viewport: 16 − 0.0027778 × 360 = 16 − 1 = 15px, and 15 ÷ 16 = 0.9375rem.
Step 4, the ends. 16 ÷ 16 = 1rem and 19 ÷ 16 = 1.1875rem.
Put together: clamp(1rem, 0.9375rem + 0.2778vw, 1.1875rem).
The check is to put viewport widths back in. The preferred value is 15px plus 0.0027778 times the viewport:
| Viewport | Preferred value | clamp() returns |
|---|---|---|
| 320px | 15 + 0.89 = 15.89px | 16px, the minimum |
| 360px | 15 + 1 = 16px | 16px |
| 768px | 15 + 2.13 = 17.13px | 17.13px |
| 1440px | 15 + 4 = 19px | 19px |
| 1920px | 15 + 5.33 = 20.33px | 19px, the maximum |
The next step up works the same way with bigger numbers. Narrow: 16 × 1.2 = 19.2px. Wide: 19 × 1.25 = 23.75px. The slope is 4.55 ÷ 1080 = 0.0042130, or 0.4213vw. The intercept is 19.2 − 0.0042130 × 360 = 19.2 − 1.5167 = 17.6833px, or 1.1052rem. The ends are 19.2 ÷ 16 = 1.2rem and 23.75 ÷ 16 = 1.4844rem.
Those two lines, as the tool writes them in its CSS output at the default settings (it emits every step, largest first, with the trim rule after them):
:root {
--step-md: clamp(1.2rem, 1.1052rem + 0.4213vw, 1.4844rem);
--step-base: clamp(1rem, 0.9375rem + 0.2778vw, 1.1875rem);
}
h1, h2, h3, h4, .trim {
text-box: trim-both cap alphabetic;
}
The ends are in rem so that a reader who has set a larger default font size in their browser still gets it. Only the middle term uses vw. The text-box rule removes the space a font builds in above the capitals and below the baseline, so a heading sits on its box. It reached Baseline across all browser engines in August 2026.
Why the two ends use different ratios
At five steps up, the narrow end is 16 × 1.2⁵ = 16 × 2.48832 = 39.81px and the wide end is 19 × 1.25⁵ = 19 × 3.0518 = 57.98px. Had the phone used 1.25 as well, its largest heading would be 16 × 3.0518 = 48.83px, 9px taller on a screen 360px wide. A flatter ratio at the narrow end keeps the top of the scale usable on a phone without making the desktop scale timid.
The size you set is not the size you see
font-size sets the em box, which is invisible. What a reader sees is the letters inside it, and their proportions belong to the typeface. Cap height is the height of a flat capital such as H, given as a fraction of the font size.
The tool’s font data gives Inter a cap height of 0.728 and an x-height of 0.546, and EB Garamond 0.650 and 0.400. Set the same scale in each:
| Typeface | Capitals at 57.98px | x-height at 19px | Body characters per line at 1440px |
|---|---|---|---|
| Inter | 57.98 × 0.728 = 42.21px | 19 × 0.546 = 10.37px | 98 |
| EB Garamond | 57.98 × 0.650 = 37.69px | 19 × 0.400 = 7.60px | 122 |
Matching Inter’s 42.21px capitals in EB Garamond takes 42.21 ÷ 0.650 = 64.94px. The same clamp() values make two different-looking scales, and that is why the tool prints the cap-height and x-height beside each step. For how far x-height varies across families, see x-height across 872 font families.
The failure clamp() hides: line length
A fluid font size and a column set as a percentage of the viewport grow at different rates, so the line gets longer as the screen gets wider, and nothing on the page looks broken.
At the tool’s defaults (Inter, 19px wide body, column at 62% of the viewport), Body line length reads 29, 58, 74, 89 and 98 characters at the five widths, each measured at the body size the clamp() gives at that width. The last figure is 1440 × 0.62 = 892.8px of column, divided by Inter’s average character width of 0.478 × 19 = 9.082px, which is 98; the first is 360 × 0.62 = 223.2px at the 16px narrow-end body, 223.2 ÷ (0.478 × 16) = 29. WCAG 1.4.8, an AAA criterion, puts the ceiling at 80. Two of the five widths are over it. (Until October 2026 the sweep measured every width at the wide-end size and read 25 at the narrow end; writing this guide caught it.)
The narrower typeface makes it worse. EB Garamond’s average character is 0.385 em, so the same column holds 892.8 ÷ (0.385 × 19) = 122 characters.
The fix is to cap the column, not shrink the text. The tool’s suggestion at the defaults is 0.478 × 19 × 66 = 599.4px, which is 599.4 ÷ 16 = 37.46rem, and its Cap at 37.46rem button copies that as a max-width line.
Other ways to do it
There is no built-in operating-system method for building a type scale. The arithmetic above is the alternative, and it fits in a spreadsheet: one row per step, one column each for the narrow size, wide size, slope, intercept and the finished clamp(). What a spreadsheet will not give you is the cap-height and the line length, because those need the font’s own metrics.
Checking what leaves the page
The scale is calculated in the tab. The only request it makes is for the typeface you pick: for any family other than a handful of common system fonts such as Arial and Georgia, it loads the font stylesheet from Google Fonts, which means the family name is sent. Your sizes and ratio are not. They sit in the address bar after a #, the part of a URL a browser does not send, and you can confirm that in the developer tools Network panel while you move the sliders.
Next
Open the Type scale generator with your own typeface and body size. To find the smallest body size your colours and font can carry, use the Type legibility checker first.