Com­pressed Flu­id Typography

When it comes to web typography, I’m a sucker for fluid type. I love that it creates a harmonious rhythm for the typography of a project. I love how it speeds up the responsive design process in the browser. And that it feels like you are working with the grain of the web, not against it. Instead of trying to control every typographic detail, you are defining boundaries that make sure your design works well – regardless of the end device. Fluid type is a textbook example of what Jeremy likes to call declarative design.

Fluid type can sometimes also be tricky, though. I’ve worked with teams who struggled with it or even abandoned it completely because it didn’t work for their more imperative design process. One of the issues that some people have with it, for example, is that you have to be fine with giving up control over how large your typography is on a particular screen. That uncertainty is something not everyone feels comfortable with. But once you do you don’t want to go back.

Basic Assumptions

But there’s another issue that’s not so easily dismissed: how fluid type responds to changes in the base font size. By default, most browsers set this base to 16px, and many fluid typography solutions simply assume that value when performing their calculations. Just take the most common approach to fluid type, using rem units and clamp():

font-size: clamp(1rem, 0.6522rem + 1.7391vw, 2rem);

We add a fraction of the viewport (or container) width to the base font size. Using rem ensures that our text remains responsive to any user-defined font size changes. And the clamp() ensures the font-size never gets smaller than our minimum and never larger than the maximum value.

This idea of fluidly scaling our type is then combined with a much older concept: the typographic scale. In the sixteenth century, European typographers developed proportional systems of type sizes to create harmony between different levels of text. The principle is simple: numbers related by consistent ratios feel balanced, much like intervals in music.

Fluid type 001

In a modular scale, we use a multiplier to generate a set of harmonious values. For example, a multiplier of 2 produces an “octave.” Or, if we use 1.5 as our multiplier, we get a “perfect fifth“.

Fluid type 003

On the Web, these calculations almost always start from the assumption that the base font size is 16px. We use multiples of 16 in our design files, we use multiples of 16 in our CSS, we are treating 16px almost as a universal standard. And while that assumption usually holds true, it only works – until it doesn’t.

Assuming that 16px is always the base font size for fluid typography in CSS is problematic because it overlooks how people actually use the web. Many users adjust their browser’s default font size for comfort or accessibility. And the moment someone bumps their default up to 24px, our carefully tuned “fluid” type system can quickly fall apart. If we are using rems – which we should – the type will scale up, which is great. But headlines that were already large can become comically huge, throwing off visual hierarchy and breaking the overall design.

Authors and Users

This raises an important question: how do we balance author intent with user preferences? As a designer, might want a headline to feel bold and impactful. That’s a legitimate aesthetic choice. But if someone looking at my design in the browser prefers to read my text in a larger font size, does this also mean that the whole typographic scale has to grow and increase to an almost unusable size? That probably wasn’t my intent at all. Ideally, our designs should respect someone’s preferred base font size without becoming unusable at larger settings. That’s why I was curious about what Miriam had to say about it. And wow, she really dives deep! I highly recommend reading her series of blog posts on the Oddbird site, in particular Designing for User Font-size and Zoom.

Adding a Scale Factor

So, I was reading through Miriam’s excellent articles and thought about her idea of negotiating between user settings and design intent using expressions like max(1em, 20px) or clamp(1em, 20px, 1.25em), when all of a sudden a question popped into my head: now that we can divide by units in CSS calc() – which is already possible today thanks to Jane Ori’s clever tan/atan2 hack and likely to be supported natively soon in all browsers – couldn’t this be a way to create a weighted fluid type scale? Or, to use a metaphor from audio engineering: a compressed fluid type scale? Here’s the idea: instead of multiplying the base font size by a ratio and letting it grow ever larger, what if we could introduce a damping factor into the calculation that compresses the fluid type scale the larger the base font size gets? It could look like this, for example, dividing the assumed base font size by the actual base font size:

:root {
  --scale-factor: calc(16px / 1rem);
}

This --scale-factor changes depending on the user’s base font size setting. At the default 16px, it evaluates to 1, so there’s no change. But if the base font-size is changed, for instance to 24px, the scale factor is now 16px / 24px = 0.666, effectively compressing the scale and preventing oversized text from overwhelming the layout.

Please be aware that division between typed values to produce a unit-less number isn’t yet possible in all browsers at the time of writing this article. Chrome added it in version 140, Safari in version 26, Firefox doesn’t support it yet, for example. So make sure to use aforementioned technique by Jane Ori as a fallback, for example.

Now that we have this —scale-factor, we can start integrating it into our fluid type calculations. The idea is to multiply a usual fluid formula by this factor, so the overall growth rate of the typography slows down as the user’s base font size increases. Take this basic example:

font-size: clamp( 1rem, 1rem + (3vw * var(--scale-factor)), 3rem);

Here, the var(--scale-factor) acts as a kind of “compressor” for the fluid range. At the default base size, it doesn’t change anything – the text scales just as before. But as the base font size grows, the scale factor reduces the influence of the viewport width (3vw in this case), so headings stay visually balanced rather than scaling up uncontrollably. If we apply this approach to a set of fluid type calculations, it will reduce the ratio of the type scale, which we assume to be what we want. The body copy text can still grow. Effectively, we are reducing the “dynamic range“ of our typography.

Bringing It All Together

And if you want, you can put all of this into a calculation that’s split up into individual custom properties. This definitely makes it look more intimidating – but it is also mich nicer to work with and easier to adjust parameters to your liking.

:root {
  
  /* This is our scaling factor based on the user font size */
  --scale-factor: calc(16px / 1rem);
  /* How aggressively should we compress the scale? 1 = full compression, 0 = no compression */
  --compression-strength: 0.7;
  
  /* The adaptive ratio multiplier that combines scale factor and compression strength and dampens the type scale at larger user font sizes */
  --ratio-dampener: calc(1 - ((1 - var(--scale-factor)) * var(--compression-strength)));
  
  /* A few ratios to choose from */
  --minorSecond: 1.06666; /* Semitone: 16:15 */
  --majorSecond: 1.125; /* 9:8 */
  --minorThird: 1.2; /* 6:5 */
  --majorThird: 1.25; /* 5:4 */
  --perfectFourth: 1.33333; /* 4:3 */
  --augmentedFourth: 1.414; /* √2/1 */
  --perfectFifth: 1.5; /* 3:2 */
  --minorSixth: 1.6; /* 8:5 */
  --goldenRatio: 1.618; /* (1+√5):2 */
  --majorSixth: 1.66666; /* 5:3 */
  --minorSeventh: 1.8; /* 9:5 */
  --majorSeventh: 1.875; /* 15:8 */
  --octave: 2; /* 2:1 */
  
  /* Base values for font-size, ratio, viewport widths */
  --baseFontSizeMin: 1rem;
  --ratioMin: var(--majorSecond);
  --minViewportWidth: 320;
  
  --baseFontSizeMax: 1.3125rem;
  --ratioMax: var(--perfectFourth);
  --maxViewportWidth: 1800;
  
  /* Apply dampening to the ratio spread */
  --compressedRatioMin: calc( 1 + (var(--ratioMin) - 1) * var(--ratio-dampener) );
  --compressedRatioMax: calc( 1 + (var(--ratioMax) - 1) * var(--ratio-dampener) );
  
  /* Min and max sizes of the scales are calculated based 
  on the provided ratio with pow(): 
  https://developer.mozilla.org/en-US/docs/Web/CSS/pow */
  
  /* Scale min */
  --size-min-4: calc(var(--baseFontSizeMin) * pow(var(--compressedRatioMin), 4));
  --size-min-3: calc(var(--baseFontSizeMin) * pow(var(--compressedRatioMin), 3));
  --size-min-2: calc(var(--baseFontSizeMin) * pow(var(--compressedRatioMin), 2));
  --size-min-1: calc(var(--baseFontSizeMin) * pow(var(--compressedRatioMin), 1));
  --size-min-0: calc(var(--baseFontSizeMin) * pow(var(--compressedRatioMin), 0));
  
  /* Scale max */
  --size-max-4: calc(var(--baseFontSizeMax) * pow(var(--compressedRatioMax), 4));
  --size-max-3: calc(var(--baseFontSizeMax) * pow(var(--compressedRatioMax), 3));
  --size-max-2: calc(var(--baseFontSizeMax) * pow(var(--compressedRatioMax), 2));
  --size-max-1: calc(var(--baseFontSizeMax) * pow(var(--compressedRatioMax), 1));
  --size-max-0: calc(var(--baseFontSizeMax) * pow(var(--compressedRatioMax), 0));

  /* This is how the fluid value is calculated: calc([value-min] + ([value-max] - [value-min]) * ((100vw - [breakpoint-min]) / ([breakpoint-max] - [breakpoint-min]))) */
  
  --fluid-4: clamp(var(--size-min-4), var(--size-min-4) + ( var(--size-max-4) - var(--size-min-4) ) * ((100vw - var(--minViewportWidth)) / (var(--maxViewportWidth) - var(--minViewportWidth))), var(--size-max-4));
  --fluid-3: clamp(var(--size-min-3), var(--size-min-3) + ( var(--size-max-3) - var(--size-min-3) ) * ((100vw - var(--minViewportWidth)) / (var(--maxViewportWidth) - var(--minViewportWidth))), var(--size-max-3));
  --fluid-2: clamp(var(--size-min-2), var(--size-min-2) + ( var(--size-max-2) - var(--size-min-2) ) * ((100vw - var(--minViewportWidth)) / (var(--maxViewportWidth) - var(--minViewportWidth))), var(--size-max-2));
  --fluid-1: clamp(var(--size-min-1), var(--size-min-1) + ( var(--size-max-1) - var(--size-min-1) ) * ((100vw - var(--minViewportWidth)) / (var(--maxViewportWidth) - var(--minViewportWidth))), var(--size-max-1));
  --fluid-0: clamp(var(--size-min-0), var(--size-min-0) + ( var(--size-max-0) - var(--size-min-0) ) * ((100vw - var(--minViewportWidth)) / (var(--maxViewportWidth) - var(--minViewportWidth))), var(--size-max-0));
}

/* And this is how we could apply the sizes */
  
h1 {
	font-size: var(--fluid-4);
}
	
h2 {
	font-size: var(--fluid-3);
}
  
h3 {
	font-size: var(--fluid-2);
}
  
h4 {
	font-size: var(--fluid-1);
}
  
p {
	font-size: var(--fluid-0);
}

Did I mention that it looks a bit overwhelming? 😂 Have a look at this CodePen prototype – which goes even further – to see it in action:

https://codepen.io/matthiasott/pen/RNrLVbP

Now, as I mentioned, the cool thing is, that if we don’t change the base font size, the fluid type works as usual.

Screnshot of the Fluid Type CodePen at the default font size of 16px

But when we increase the base font size to 24px – for example, by setting Chrome’s “Appearance” → “Font size” option to Very Large – the type scale also responds as expected: the ratio becomes smaller, large headings no longer grow disproportionately, and the body text scales up nicely. And the nice thing is that scaling up our body copy to 200 % by zooming in the browser also works, because our fluid type calculations are still based on rem units.

Screenshot of the CodePen with the base font size set to 24px and a smaller ratio

That said, it’s worth noting that this approach currently is an experiment and does not fully satisfy the WCAG SC 1.4.4 Resize Text accessibility requirement, because although the smaller non-display sizes can be scaled up to 200 %, the larger headings can’t. So if you are considering using such an approach in production and you need to fulfil the WCAG accessibility, either don’t do it or provide a way to scale up the headings as well. Thanks to Adrian Roselli for the hint. Also, Safari handles zoom and font scaling slightly differently from Chromium or Firefox. Larger font sizes can appear more exaggerated there, likely due to the way Safari recalculates computed styles during zoom. So always be testing.

And that’s it. The first iteration of this idea. It’s definitely still experimental, but I wanted to share it in case others find it useful or inspiring. Maybe it sparks new ideas about how to make fluid type more adaptive and user-friendly. And I guess it will mature in my head over time – and if you know of someone who has explored something similar (or even better), I’d love to hear about it. There’s nothing new under the sun, you know…

Ultimately, it really feels like CSS has reached a point where we no longer need to chase the perfect solution for things like fluid typography. There’s no single technique to rule them all. Instead, we have an ever-growing toolkit that lets us be creatively combine approaches and bring together design and engineering to tailor our solutions to fit each project’s needs. And, of course, experiment with fluid type on our personal sites.

This is post 12 of Blogtober 2025.

~

53 Webmentions

Photo of Frontend Dogma
Frontend Dogma
Compressed Fluid Typography, by @matthiasott: https://matthiasott.com/notes/compressed-fluid-typography #typography #css #fluiddesign css fluiddesign typography Compressed Fluid Typography · Matthias Ott
Photo of damianwalsh
damianwalsh
@matthiasott This is cool—thanks for writing up your discoveries. I've previously reached for utopia.fyi to avoid getting to grips with the calculations involved, but you've inspired me to look again. Learning is fun!
Photo of Matthias Ott
Matthias Ott
@aardrian Thanks Adrian! 🤗 I think I fixed the Firefox issue now. And I’ll check if I can improve the scaling issue.. Regarding the resize text requirement: does it also require text that is already at a font size of 70px to scale up to 140px? I understand that it is a success criterion, esp. in terms of non-display sizes – I’m just asking myself what the real-world use of such a rule might be for ...
Photo of Matthias Ott
Matthias Ott
@nicod @nhoizey I think I fixed it in Firefox now. Firefox doesn’t support division by numbers with units yet and there still was an error in one of the calc() functions…
Photo of Adrian Roselli
Adrian Roselli
@matthiasott I loaded it to see if it still allows 200% scaling for all text to conform to WCAG SC 1.4.4 Resize Text. In Chrome, it breaks down at H3 (neither it nor H2 nor H1 allow me to get to 200% of the original size). I know headings and WCAG are a frustration point (there are open issues), but still a concern. As you already know, this doesn’t work in Firefox.
Photo of Matthias Ott
Matthias Ott
@kizu To be honest, I haven’t had the time to explore it in more depth yet beyond the initial idea. 😅 At the moment, the h1 still grows a bit, but maybe one could add a max somehow, yes. Or calculate it so that the “compression” factor is set so that the h1 effectively doesn’t change/grow… 🤔
Photo of Roma Komarov
Roma Komarov
@matthiasott I had some drafts about very similar stuff, haha. One thing that I want to check/try: if we have the scale factor, will your clamps ever reach the max boundary? And another thing I wanted to try (maybe yours is already kinda it?): have responsive min, but not responsive max, with the scale being from the min size in rem to our ideal max in px. Then, when increasing the rem, the min will grow at 100% ...
Photo of Matthias Ott
Matthias Ott
@sl007 @nhoizey I don’t know why but I never felt comfortable using a baseline grid in print (QuarkXPress anyone? 😂) – it always felt wrong when I placed images – so I never really tried to build one in CSS. But that’s really just my personal preference, I see the value and get why it can be useful. I’d probably try using the `lh` unit today … 🤔
Photo of Sebastian Lasse
Sebastian Lasse
@nhoizey @matthiasott sane in my ff - until tonight (everything will be alright tonight, no one moves, no one grooves, no one thinks, No one walks und so) just btw what is the best way to have a solid baseline in 2025/2026 (e.g. for multi-column layouts or masonry grids)? made a calculator once … https://codepen.io/sebilasse/pen/BdaPzN Typographic Rythm adjusted to a solid baseline!

22 Likes

14 Reposts

ⓘ Webmentions are a way to notify other websites when you link to them, and to receive notifications when others link to you. Learn more about Webmentions.