Skip to content
Typography

Fluid Typography Must Be the Woodstock of CSS

Even my mom wrote about fluid typography — but are you aware of what else it unlocks?


I Thought I Had Invented It

Okay, I didn’t.

I’d read about this way back. I was just waiting for the right moment to implement it. That moment finally came—and despite the simplicity of the idea, I struggled.

If you’re here to learn about fluid typography, you’re reading the right article. But there’s something we need to address first.

I don’t know how you landed here, but if it was by fluke, you need to know: there are ten full Google search result pages of deep-dive articles on this subject. Have you ever seen ten pages of links of in-depth writing about a single CSS concept? Not clickbait. Not SEO fluff. Actual insight.

And apparently, there are calculators too. Loads of them. Entire pages of sliders and inputs just to generate clamp() values for font sizes.
What more could be said?

Turns out, a lot.

Before I realized this topic was basically the French Revolution of CSS, I was already planning to write about it—not just to share the approach, but to reflect on what it taught me about CSS itself.

Because here’s the thing:

Yes, fluid typography is nice. But the bigger takeaway is what you can do when everything is sized relatively.

This article will explain fluid typography—but it’s also about rem-based scaling more broadly. And it’s about the surprising limitations I ran into when combining CSS’s most advanced tools. To be completely honest, those limitations were compounded by my own misinterpretations—and we’ll talk about that too, because I think there’s something to learn.

TL;DR

Just want a solution?

# Napkin Notes

## Concept
320px  <---------->  1280px
16px   <---------->  20px

## Ranges
1280 - 320 = 960
20 - 16 = 4

## Rate of increase / decrease

> 1 *divided* by viewport range *multiplied* by font size range

1 ÷ 960 × 4
0.00416666666666

## Size at viewport width of 0

> Smallest font size *minus* smallest viewport width *times* rate of increase

(16 - 320 * 0.0041666666666) / 16
0.916666666667

## CSS

html {
  font-size: clamp(1rem, 0.4167vw + 0.9167rem, 1.25rem)
}

It Seems So Simple

And by simple I only mean it’s a one-liner.

But if it’s so simple, how can there be ten pages of articles on the same subject?

Here are a few questions you might be asking after reading those napkin notes or reading other articles:

  • How does that math work?
  • What happens if the user has customized their browser’s font size?
  • Will it still work when zoomed?
  • How is zoom different from a browser font size increase?
  • Why do other articles use calc() and clamp() many times over?
  • Why do other articles use stepped variables instead?
  • Why does this example only set font-size on the html element?

If you’re wondering about any of these — or if you want to go deeper into what fluid typography really allows, along with the limitations I ran into using CSS calc(), var(), and units together — read on.

The px Is Not a Pixel

We need to clear this up before going any further.

Pixels — the tiny dots that make up a screen — vary in size. On older displays, you could sometimes see them individually if you got close enough. On modern high-density screens, they’re often too small to distinguish. A small smartphone screen might pack in more pixels than a large desktop monitor. This is called pixel density, or more precisely, pixels per inch (PPI) or pixels per distance (PPX).

But here’s the twist: the px unit in CSS doesn’t refer to a literal hardware pixel.

Today, 1px in CSS is defined as a physical length — specifically, 1⁄96 of an inch, or approximately 0.2646mm. Browsers use information about the screen’s pixel density to scale that unit appropriately.

Now, about fonts:
Each character sits inside an invisible box called an em square. The height of this box is set by the font-size property, but the actual appearance of the letters is defined by the font designer. That’s why some fonts look “bigger” or “smaller” than others, even at the same font-size.

Fortunately, most fonts used in web design have predictable proportions — which makes relative scaling practical and consistent.

How Does the Math Work?

It seemed magical at first, but it’s easy enough to grasp.

Let’s bring back a visualization of the concept:

## Concept
320px  <---------->  1280px
16px   <---------->  20px

We start with 16px and want it to scale up to 20px as the viewport widens.

To do that, we just add small amounts to the font size as the viewport width increases — a smooth, linear growth.

So how do we express that?

Easy: add a small percentage of the viewport width.

That’s it.

Done!

…Well, almost.

You’d be right to think I’ve left out some details.
Read on.

The Formula Alone — Without clamp()

The idea is to scale between 320px and 1280px — but the formula itself doesn’t know that. It just applies a rate of change, starting from 0 to infinity.

In our case, that rate is 4px per 960px — or 0.0041667px per 1px — of viewport width.

To hit 20px at 1280px, we work backward:

1280 × 0.0041667 = 5.333px 
20 - 5.333 = 14.667px → 0.9167rem

So the formula is:

html {
  font-size: 0.4167vw + 0.9167rem;
}

/* 
 * `vw` is 1/100 so multiply the rate of change by 100
 */

You can then wrap it in clamp() to enforce bounds — but that’s optional.

clamp() is optional
The math works fine on its own in many cases — especially if you don’t mind the size scaling beyond the original range.

What About Zoom — Or Browser Font Size Customization?

Zoom is great. It scales the entire page proportionally — text, layout, everything. And it remembers the zoom level per site, which is a bonus. When you implement fluid font scaling, zoom behaves exactly as you’d expect. No issues.

But browser font size customization is a different story.

That setting increases the font size — but not the rest of the UI. It’s meant to improve readability, especially for users with low vision. And this is where fluid font scaling reveals its true potential.

In fact, this is why fluid typography should be a subsection of something larger: rem-based scaling. But we’ll get to that.

I’m referring specifically to the font size setting under:

Menu → Settings → Font Size
varies by browser

No, it’s not a setting most users touch — but for those who do, it matters.

If you use px-based font sizes, you block this setting from doing anything on your site. But if you use rem, you respect the user’s preference.


So does fluid scaling still work when users customize their font size setting?
Yes. It works perfectly — because it’s all relative to the root.

But What Are REMs, Anyway?

Our examples have only modified the font-size of the root element — and that’s the point. It works out of the box. But in real-world projects, you’ll almost always customize font sizes elsewhere.

To keep fluid typography working consistently, you need to use rem for all font-size declarations.

Why?

Because rem always refers to the font-size of the root element — :root or html.

Sidebar

  • :root refers to the root of the document
  • html refers to the <html> element
  • You could use them interchageably
  • :root takes precedence over html
  • :root is preferred for defining variables
  • html is preferred for everything else

If you use any other unit — like px — that element’s font size won’t scale with the rest of the interface.

There may be edge cases where you may want this, but I haven’t run into one yet.


An exception to this rule is em.

Unlike rem, which always references the root, em is relative to the nearest ancestor with a font-size. That might be the root, but it could be any parent in the chain.

So what happens if that ancestor uses rem or em?
Then the scaling still works — just indirectly.

But if that ancestor uses px?
The scaling chain is broken. Fluid scaling no longer flows through.


Also of note:
You can use rem and em for any size — not just fonts. Properties like width, height, margin, and border-radius will respond just the same.

  • rem always refers to the root’s font-size
  • em always refers to the element’s own font-size, whether directly set or inherited

There are use cases for everything — and until you’re in that situation, you might not see the need.

But don’t solve problems you don’t have.

In most cases, one calc() is enough. From there, use rem. That’s the whole point.

That said, you might need:

  • Fonts that scale at different rates
  • More than one responsive range
  • Breakpoint-specific overrides
  • Custom UI zooming

In those cases, you can:

  • Use calc() directly in specific selectors
  • Or define CSS variables for your special cases and reuse them as needed

Keep it simple — until you have no other choice

REM-Based Scaling

I said REM-based scaling was the bigger picture.
I said fluid typography was just a subset.
And now here I am — wrapping up the larger article with the smaller part. Ha!

Truth is, the entire setup just scales a value that happens to be font-size.
That value is exposed as rem. But even though it’s called font-size, you can treat it as just-a-value.

You can use it for font-size, of course — but you can use it for everything else too.

Everything becomes a proportion of font-size:

  • A border-radius of 4px? → border-radius: 0.25rem
  • A centered <main> that’s normally 960px? → width: 60rem
  • An SVG logo that scales like headings? → height: 2rem

Once the root scales, everything else scales with it.

A Scaling border-radius

This is a great example.

When a <div> or a <button> gets smaller,
a fixed border-radius can start to feel too round — especially on compact layouts.

But if the border-radius scales automatically with the UI,
there’s no need for extra @media rules. It just works.

A Scaling <main> Width

But you want a fixed width, don’t you?

I get it. But don’t knock it till you try it.

What I absolutely love about this:
If your centered column scales with the text size,
the line breaks don’t budge.

Paradoxically, a width that scales ends up feeling
more “fixed” than a fixed-width column.

An SVG Logo That Scales Like Headings

This too, I love.

We often reach for @media rules with px-based widths and heights,
or use percentages relative to a parent container.

But if you already know the font-size of the surrounding text,
you also know what rem size to give your SVG.

And just like that — it scales with everything else.
No extra @media rules needed.

Zooming and Browser Font Size

I’m not sure which of these examples I love most — but this one comes close.

Remember how browser zoom scales everything proportionally?
When you use rem for all your sizing, you’re effectively doing the same thing:
you’re zooming — but within bounds you control.

Now think back to the browser font size setting —
the one that increases text size without scaling the UI.
It can make a mess of layouts.

But if you’ve used rem everywhere, then you’re supporting that setting
better than anyone else.

Why?

Because while other sites might look broken or confusing when the setting is bumped up,
yours will scale cleanly.
It’ll look just like it always does — only bigger.

CSS Frustrations

Oh. My. God.

Warning to developers: this one’s for you.

CSS is not code.

Ah shoot — here come the pitchforks.

Okay, okay — CSS is code.
But don’t think in JavaScript terms.

You’ll see CSS variables.
You’ll see calc().
You’ll feel at home — and start thinking order matters.
I certainly did not do that. No, I certainly did not.

Folks, have you ever CSS’d so bad you caused a stack overflow?
No?
Well, I did.
(More on that later.)

Now for the kicker:
calc() cannot multiply or divide two values with units.
One of them must be unitless — see the MDN Web Docs for details.

That limitation frustrated me — so I went looking for a reason.
And I found this elegant explanation:

“Because you can’t multiply lengths in the real world…”

As elegant as that is — and yes, I agreed with it at the time —
it’s not really about the real world.

CSS could drop the unit when needed.
It’s about intention — not physics.

And guess what?
They might.
It’s being discussed.

Combining CSS Variables, calc(), and rem

It’s the Bermuda Triangle, people.

You’ll see variables, calc(), and rem
and your instincts will be to combine them into a clean, efficient, modular pattern.
The kind of pattern you always implement.

You’ll arrive at a pretty nice solution — I have no doubt.
But then the limitations start surfacing.

They’re subtle, but if you pay attention to detail, you’ll catch them.

I’m not talking about the one-line solution we used earlier — or the ones in other articles.
Those keep away from the danger zone.
They’re concise, easy to grasp, efficient.
They avoid CSS’s weirdness.
And they work.
(Which is why I can confidently recommend them — I didn’t invent them, remember?)


Now, here’s what I tried initially:
Instead of doing the math on paper, I wrote the formula directly in CSS.
I wanted it to be pluggable — drop in your numbers, and let CSS do the math.

Do not do that.

Normally, you won’t run into problems — because rem is static.

But in fluid typography, we are dynamically updating rem
(by scaling the root font-size with clamp() or calc()).

That changes everything.

Because now, when you use rem inside a variable,
and use that variable inside a calc(),
and the calc() affects font-size,
which in turn affects rem

You’ve just created a loop.
A recursive loop.
That’s why I joked earlier about causing a stack overflow.

In reality, the CSS engine bails you out —
it detects the loop and stops it.

But the point remains:

If your font-size is dynamic, your math should be static.

So, can the formula still be pluggable?
Yes — but calculate the values outside CSS.
Both factors — the rate of change and the starting value at 0px
should be computed ahead of time.

If you want something programmatic, use a preprocessor like SCSS.

Let CSS do what it does best: apply styles.
Let you do the math.

Why Fluid Typography Sucks

(Kind of.)

Don’t get me wrong — I love fluid typography.
I’ve written an entire article praising it.
But before you rebuild your whole UI on a rem-based utopia, let’s talk about where things get weird.


1. rem-based Media Queries Aren’t What You Think

You’d expect @media (min-width: 60rem) to scale with your fluid root font size, right?
It doesn’t.

Media queries based on rem use the browser’s default base font size — usually 16pxnot your dynamically computed font-size.

So if you scale your root font with clamp(), your breakpoints won’t scale with it.
They’ll stay rigid.


2. The sizes Attribute Ignores Dynamic rem

The same gotcha applies to the sizes attribute in responsive <img> tags.

When you write something like:

<img 
  srcset="image-480.jpg 480w, image-960.jpg 960w"
  sizes="(min-width: 60rem) 960px, 100vw"
>

That 60rem is also tied to the browser’s static base font size, not your dynamic root. So your beautifully fluid rem setup won’t carry over to images unless you do extra work — or avoid rem in these places altogether.


3. You Might Not Want Everything to Scale

Lastly — scaling everything isn’t always the best design choice.

You might prefer:

  • Fixed column widths for alignment consistency
  • Step-based typography for predictable layout breaks
  • A classic breakpoint-based UI that snaps instead of flows

And that’s totally valid.

Fluid typography is powerful, but it’s a tool — not a law. Know when to use it, and know when to stop.


Final Thought

I set out to scale a UI.
What I found was a rabbit hole of rem, calc(), variables, font-size settings, and more blog posts than I thought the internet could hold.

But in the end, the lesson was simple:

Scale one thing well — and let the rest follow.

That thing is font-size.
And if everything else uses rem, the whole UI becomes fluid, accessible, and easier to maintain.

Fluid typography is just the visible part.
The deeper idea is rem-based thinking
one source of truth, applied everywhere.

So do the math once.
Use rem everywhere.
And don’t let CSS trick you into overcomplicating it.

The best part?
It works.


How am I doing?

I care about making these articles better. If something was unclear, helpful, surprising—or if you just want to say hi—you can leave a comment on the discussion thread below.

This happens on our forum (Discourse), so you may be asked to log in or create an account.