Fluid type scales in practice: from ratio to tokens

Vintage brass letterpress type blocks arranged in an ascending scale on cream paper, with fern-green ink smudges — a physical metaphor for a fluid type scale.

Every marketing site has two type scales. The one in the design file, with its tidy modular ratio, and the one in production, where the 64px headline that looked authoritative at 1440px is shouting over two lines at 375px and somebody has hot-patched it with a media query that disagrees with the other three media queries.

Fluid type is the grown-up answer: instead of stepping between discrete sizes at breakpoints, each level of the scale interpolates continuously between a minimum and a maximum. Done well, it feels like the typography was set specifically for whatever width the reader happens to hold. Done badly, it feels like the page is breathing. This is how we do it well on our marketing site builds — the ratios we start from, the clamp() mechanics, the optical corrections nobody ships by default, and the token layer that keeps the whole thing legible to the next team.

Start with two ratios, not one

The classic approach picks one modular ratio — 1.25, the major third, whatever feels musical — and applies it at every viewport. The problem is geometry. On a 1440px canvas you have room for dramatic contrast between body and display: a 1.333 ratio gives you a proud headline. On a 375px phone that same ratio produces either a headline that wraps four times or a body size too small to read comfortably.

So we design with two related scales:

  • Mobile scale: ratio ~1.2 (minor third). Gentle steps, because horizontal room is scarce and the difference between "heading" and "slightly bigger heading" does the hierarchy work that raw size can't.
  • Desktop scale: ratio ~1.28–1.333. Expressive at the top, because the top steps exist to be expressive.

The scales share one anchor: body text, which stays nearly constant (more on why below). Every other step is defined as a min from the mobile scale and a max from the desktop scale, and the browser interpolates. A starting set we use, in pixels, before tokenising:

StepMin (375)Max (1440)Role
step--11415Captions, meta, labels
step-01719Body
step-12024Lead paragraphs, h4
step-22431h3
step-32941h2
step-43555h1, section display
step-54272Page display
step-65096Hero display

Don't marry these numbers. Ratios are a starting position, not a religion — the final scale gets tuned by eye, because some steps are read as pairs and their relationship matters more than their ratio.

The clamp() mechanics, written to be read

The interpolation itself is one line of CSS per step, but the line everyone copies from a generator is illegible:

font-size: clamp(2.1875rem, 1.4346rem + 3.1454vw, 4.5rem);

That's the right answer in an unreadable form. Six months later nobody dares touch it. We generate the same values but keep the meaning in comments and custom properties:

:root {
  /* step-4: 35px @375 → 55px @1440. Slope ≈ 1.88vw + 28px. */
  --step-4: clamp(2.1875rem, 1.752rem + 1.878vw, 3.4375rem);
}

A few rules that keep the mechanics honest:

  1. Use rem, always. Values in rem respect the reader's root font-size preference; px values override it. This is an accessibility requirement wearing an implementation costume.
  2. Keep the viewport range explicit. We interpolate between 375 and 1440 and freeze outside it (that's what the min/max of the clamp are for). Wider than 1440, type stops growing — a 96px display at 2560px wide is a billboard, not a headline.
  3. Document the slope. The middle term (1.752rem + 1.878vw) encodes the rate of growth. A comment with the two lock points makes future edits arithmetic instead of archaeology. The formula is: slope = (max − min) / (maxWidth − minWidth), then intercept = min − slope × minWidth. Write it once in the tokens file header and point every comment at it.

Tools like Utopia's calculator are excellent for generating the initial set — just treat the output as a draft that a designer then corrects, the way you'd treat a first proof from a printer.

Where fluid type goes wrong

Fluid body text. This is the most common own goal. Body copy at clamp(16px, …, 19px) sounds harmless but causes two problems: it fights the user's zoom expectations (WCAG 1.4.4 requires text to survive 200% zoom, and intermediate fluid sizes interact badly with horizontal reflow), and it makes line lengths drift, so the measure you tuned at design time exists for nobody. Our rule: body text is fixed per breakpoint family — 17px up to ~700px, 18–19px above — and only display sizes go fluid. The scale flows; the reading stays still.

The midpoint dip. Fluid sizes are only ever specified at the two lock points. We once shipped a scale where both endpoints were tuned and the 834px midpoint rendered an h1 that collided with its own testimonial band. The endpoints were perfect; the journey between them wasn't. Test at the midpoint of your interpolation range — for us that's roughly 900px — with the same rigour as the extremes.

Too many steps. Eight steps is plenty for a marketing site. CMS editors given twelve steps will use all twelve, and the page becomes classified ads. Fewer, further-apart steps make hierarchy obvious; a dense scale makes it theoretical.

Optical correction: the part generators can't do

A computed scale is a starting grid. Type at large sizes needs manual correction because perception doesn't scale linearly:

  • Letter-spacing tightens as size grows. Display steps take −0.01em to −0.03em of tracking; captions take a touch positive. Big type with default tracking looks like it's leaning backwards.
  • Line-height compresses as size grows. Body sits at 1.55–1.65; display at 1.05–1.15. We ship line-height alongside font-size in the same token — a size token without its leading is half a thought.
  • Use the optical size axis if you have one. We set most Brassfern display work in Fraunces, which has an opsz axis: font-variation-settings: 'opsz' 100 at display sizes, dropping toward 9 for captions it never quite needs. On fluid steps, bind opsz to the same clamp so the letterforms redraw their contrast as they grow — this is the closest the web gets to a punchcutter cutting a different punch for display sizes, and it costs nothing at runtime.
  • Correct the outliers. In our scale, step-3 is always the awkward teenager — too big for a subhead, too small to read as display on mobile. Every project, this step gets manually raised by a pixel or two at the min. Expect one or two manual nudges per project; a scale with zero nudges is one nobody looked at on a phone.

Pairing matters as much as scale. Our house pairing — a high-character serif (Fraunces) for display, a quiet grotesque (Instrument Sans) for interface text — works because the grotesque doesn't compete at the same sizes; the ratio gap between steps does the sorting. If your pairing needs colour and weight tricks to tell a headline from a label, the scale isn't carrying its share. And whatever you pair, the performance contract in typography that still loads fast applies: subset, preload, and never swap-stun the reader with a reflow.

Tokens that survive the next team

The scale lives as design tokens, and the token layer is where the discipline actually happens. Ours look like this in the theme file:

:root {
  --fl-step-0: 1.0625rem;                    /* 17px, fixed below 700px */
  --fl-step-3: clamp(1.8125rem, 1.436rem + 1.624vw, 2.5625rem); /* 29→41 */
  --fl-leading-body: 1.6;
  --fl-leading-display: 1.08;
  --fl-track-display: -0.022em;
}

Three conventions keep this alive:

  • Name steps ordinally (step-3), not semantically (h2). Semantic names rot the moment an h3 needs to look like a step-4. Components bind semantics; tokens supply sizes. The same separation is what keeps spacing honest in our editorial grid work.
  • Ban raw font-size values in components via a lint rule or code-review custom. A type system enforced by taste lasts one sprint; one enforced by the only available API lasts years.
  • Ship a specimen page. Every project gets a hidden /type-specimen route rendering every step at three widths. It's a five-minute build that catches regression, delights clients, and gets linked from the README next to the accessibility checklist.

We ran exactly this system on the Holloway Records label site, where the display scale does brand work — a 96px italicised release title at desktop breathing down to a composed 50px on a phone without a single breakpoint patch. The client never noticed the mechanism, which is the review you want: nobody compliments fluid type, they just never feel the page break.

Key takeaways

  • Use two related ratios — ~1.2 at mobile, ~1.28–1.333 at desktop — anchored on a body size that stays fixed.
  • Interpolate between explicit lock points (we use 375px and 1440px), freeze outside them, and comment the slope so the math stays editable.
  • Keep body text fixed per breakpoint family; fluid body copy fights zoom behaviour and line-length stability.
  • Test the midpoint (~900px), not just the endpoints — interpolation bugs live in the journey.
  • Correct optically: tighten tracking, compress leading, drive opsz with the same clamp if your face has the axis.
  • Tokenise ordinally, ban raw sizes in components, ship a specimen page.

FAQ

Is fluid type bad for performance? Negligible. clamp() is evaluated natively; there's no JavaScript, no ResizeObserver, no layout thrash. The real performance cost of typography is font files, and fluid type neither adds nor removes those.

Should we use container-based units (cqi) instead of viewport units? For page-level display type, viewport units remain right — the type responds to the window, as the reader expects. Container queries shine for component-level typography inside cards and widgets. We use viewport fluid type for the scale and container queries for component layout, and rarely need both on the same element.

How does this work with a CMS? Expose steps, not sizes: editors choose "Display", "Heading", "Subhead" from a constrained set that maps to tokens. If the CMS allows free pixel entry, no scale — fluid or otherwise — will survive the campaign season.

Does fluid type replace breakpoints? No. It removes the font-size reason for most breakpoints. Layout still needs them: columns collapse, images re-crop, navigation metamorphoses. Think of fluid type as shrinking the breakpoint list to the decisions that actually need steps.

What about print styles? Freeze everything at sensible pt equivalents in the print stylesheet; fluid units mean nothing on paper. Five lines of CSS, worth it every time someone prints a case study for a procurement meeting.


Keep reading