Blog Type Scale Alt2

Opinionated Typography: Scale

Written by Paul Woods.
Published on 14 September 2026.

Most of what you look at on the web is text. Even if you’re watch­ing videos. And there are a mil­lion dif­fer­ent takes on what makes great typog­ra­phy. I think it’s fair, then, to throw my hat in the ring.

Contents

The font scale 

There’s been a lot of debate about font scales and which ones to use. Some peo­ple go with some­thing based on a ​“tra­di­tion­al” scale inher­it­ed from met­al type foundries. You’ve seen these in things like Microsoft Word (6, 8, 9, 10, 11, 12, 16, 18, etc.).

Some just eye­ball it. But this real­ly isn’t sus­tain­able or main­tain­able in the squishy world of web design. Today’s approach is bas­ing scales on a ratio.

Two rows of letters, representing two approaches to type scales. The first uses a traditional method, and the second uses a ratio.

The top row is the legacy scale used by word processors. The bottom row uses a 1.2 scale.

The advan­tage here is that there aren’t mag­ic num­bers every­where. Just one.

This is the first thing to set up, and with a few mod­ern tech­niques, a whole font-size scale can be pro­duced in a jiffy.

:root {
  --font-size-ratio: 1.2;
  --font-size-base: 1em;
  --font-size-1: calc(var(--font-size-base) * pow(var(--font-size-ratio), 1));
  --font-size-2: calc(var(--font-size-base) * pow(var(--font-size-ratio), 2));
  /* etc. */
}

.fs-base { font-size: var(--font-size-base); }
.fs-1 { font-size: var(--font-size-1); }
.fs-2 { font-size: var(--font-size-2); }
/* etc. */

You’re prob­a­bly famil­iar with Cus­tom Prop­er­ties (--custom-prop). But what’s new here is the pow() func­tion: the pow­er func­tion. pow(x, 2) is the same as x². Mul­ti­ply­ing the base font size by the ratio to the pow­er of the step num­ber cre­ates that step’s new font size.

If you’re using Sass (SCSS), you can cre­ate full ramps in a few seconds:

:root {
  --font-size-ratio: 1.2;
  --font-size-base: 1em;
  @for $i from -2 through 3 {
    @if $i == 0 {
      --font-size-#{$i}: var(--font-size-base);
    } @else {
      --font-size-#{$i}: calc(var(--font-size-base) * pow(var(--font-size-ratio), #{$i}));
    }
  }
}

As you can see here, it works for neg­a­tive num­bers too. You can tweak the ratio, the base font-size and the num­ber of sizes you want, in both directions.

As a side note, and as a musi­cian, I like to pick some­thing from the musi­cal har­mon­ic scale as the ratio. The 1.2 here is a ​“Minor Third”.

Where it gets inter­est­ing, though, is how to deal with screen sizes.

Line height

As a bonus, this approach also works for line height. I find line heights should decrease as we go up our head­ing lev­els. Tighter spac­ing helps big head­ings feel a bit more balanced.

Example text with a heading and dummy-text paragraph. It illustrates how a tighter line height for large text feels more balanced.

The heading here uses a 1.2 line height ratio, and the body text uses 1.6.

:root {
  @for $i from -3 through 0 {
    $calc: calc(var(--line-height-base) * pow(var(--line-height-ratio), $i));
    --typography-line-height-#{$i} : #{$calc};
  }
}

His­to­ry les­son time 

Now, before we had to wor­ry about dif­fer­ent screen sizes, we all worked off 800 by 600px frames. It was all so simple!

Then Ethan Mar­cotte wrote this arti­cle back in 2010 for A List Apart, intro­duc­ing the con­cept of the ​“respon­sive web” to ter­ri­fied devel­op­ers. But it meant that one code­base could serve mul­ti­ple screen sizes. If you’re old enough, you’ll remem­ber spe­cial ​“mobile” ver­sions of web­sites that served, real­ly, a com­plete­ly dif­fer­ent web­site than the desk­top ver­sion. mob.examplewebsite.com or examplewebsite.com/mobile. Respon­sive web­sites were an alter­na­tive to ​“adap­tive” web­sites.

Media Queries

But things weren’t quite respon­sive. @media queries were intro­duced, allow­ing the same CSS file to change things in ​“steps” or at break­points. This was true of typog­ra­phy. Every head­ing had a num­ber of dif­fer­ent ver­sions. A mobile h1 and a desk­top h1, and pos­si­bly a few in between.

The BBC web­site intro­duced the idea of ​“cut­ting the mus­tard”. The idea was that users with the most basic devices were served the ​“mobile” expe­ri­ence: the UI was typ­i­cal­ly stacked. And as the devices got more capa­ble and larg­er, the more com­plex lay­outs kicked in. So, rather than design­ing for desk­top and adapt­ing to mobile, it worked the oth­er way around. This could be a whole oth­er blog, but it made sense to design ​“upwards”, with all media queries being a break­point size and above.

body { font-size: 16px; }

h1 { font-size: 20px; }

@media screen and (min-width: 600px) {
  h1 { font-size: 24px; }
}

@media screen and (min-width: 900px) {
  h1 { font-size: 30px; }
}

Of course, em or rem units are a bet­ter choice here, and em for media queries is also a bet­ter choice if you want media queries to scale with text size, but you get the idea.

Flu­id typography 

So, I said that ​“respon­sive” design, as described above, isn’t quite respon­sive. Things still hinge on break­points and steps, mean­ing that how a web­page looks can change dra­mat­i­cal­ly as soon as one of these thresh­olds is crossed.

Enter viewport units. These basi­cal­ly rep­re­sent a per­cent­age of the view­port width, height, or whichev­er is small­est or largest: vw, vh, vmin, vmax. Devel­op­ers start­ed to realise that font sizes could be set with these, essen­tial­ly mak­ing text expand or con­tract based on screen size.

The chal­lenge here is that large screens will have humon­gous font sizes, and mobile screens will have minute font sizes. The clamp() func­tion allows you to set lim­its on either side and use viewport units for the flu­id, in-between sizes.

--font-size-fluid-1: clamp(var(--font-size-1), 1.5vw ,var(--font-size-2));

There are two issues here:

  1. The 1.5vw is a bit of a guess, and real­ly a bit of a mag­ic num­ber. It is pos­si­ble, with some more cal­cu­la­tions, to work out a ​“slope” between a min­i­mum and max­i­mum view­port size, but it’s not elegant.
  2. It’s a chal­lenge get­ting this to work for user font scal­ing, espe­cial­ly if the upper lim­it pre­vents scal­ing above a cer­tain size.

Progress

progress() is a fair­ly new func­tion, but it’s pret­ty well sup­port­ed across browsers. It’s a lit­tle strange func­tion. It takes three val­ues: the amount, a low­er bound and an upper bound. It always returns a pure, unit­less num­ber. The val­ues can them­selves be unit­less or have units. This means you can take 100vw and cre­ate a ​“slope” between a min­i­mum view­port width and a max­i­mum view­port width.

progress(100vw, 400px, 1200px);

/*
At 400px, 100vw = 0
At 1200px, 100vw = 1
At 800px, 100vw = 0.5
*/

With a few para­me­ters, you can cre­ate a cal­cu­la­tion that builds a flu­id size:

:root {
  --ratio-min: 1.2;
  --size-min: 1em;
  --viewport-min: 400px;
  --ratio-max: 1.25;
  --size-max: 1.2em;
  --viewport-max: 1200px;
	
  --_progress: progress(100vi, var(--viewport-min), var(--viewport-max));
	
  --_min-0: calc(var(--size-min) * pow(var(--ratio-min), 0));
  --_max-0: calc(var(--size-max) * pow(var(--ratio-max), 0));
  --font-size-fluid-0: calc(var(--_min-0) + var(--_progress) * (var(--_max-0) - var(--_min-0)));
	  
  --_min-1: calc(var(--size-min) * pow(var(--ratio-min), 1));
  --_max-1: calc(var(--size-max) * pow(var(--ratio-max), 1));
  --font-size-fluid-1: calc(var(--_min-1) + var(--_progress) * (var(--_max-1) - var(--_min-1)));
}

Again, with a lit­tle help from Sass, you can cre­ate a loop to out­put a full font scale ramp in a flash:

:root {
  @for $i from -1 through 3 {
    --_min-#{$i}: calc(var(--size-min) * pow(var(--ratio-min), #{$i}));
    --_max-#{$i}: calc(var(--size-max) * pow(var(--ratio-max), #{$i}));
    --font-size-fluid-#{$i}: calc(var(--_min-#{$i}) + var(--_progress) * (var(--_max-#{$i}) - var(--_min-#{$i})));
  }
}

This solves both prob­lems posed by the clamp solu­tion. If you want to be a bit more back­wards com­pat­i­ble, you can do a sim­ple @supports check and have the old­er clamp solu­tion as a fallback.

@support (opacity: progress(5, 0, 10)) {
  /* progress() -based type scale here */
}

See the Pen Typography Fluid Scale Sass by Paul Woods (@paleweasel) on CodePen.

Give it a go yourself with this CodePen.

Just one more thing… 

We can make our work even more effi­cient using the @function rule, so the rea­son­ably busy cal­cu­la­tions can be con­sol­i­dat­ed into one place:

@function --type-size-fluid(--n) {
  --_min: calc(var(--size-min) * pow(var(--ratio-min), var(--n));
  --_max: calc(var(--size-max) * pow(var(--ratio-max), var(--n));
  
  return: calc(var(--_min) + var(--_progress) * (var(--_max) - var(--_min)));
}

These can then be slot­ted into our CSS like:

@supports at-rule(@function) {
  :root {
    --font-size-fluid--2: --type-size-fluid(-2);
    --font-size-fluid--1: --type-size-fluid(-1);
    --font-size-fluid-0: --type-size-fluid(0);
    --font-size-fluid-1: --type-size-fluid(1);
    --font-size-fluid-2: --type-size-fluid(2);
    /* etc. */
  }
}

Note: iron­i­cal­ly, @supports at-rule(*) is only sup­port­ed in Chrome! So use it as a pro­gres­sive enhance­ment for now. It will just be ignored in old­er browsers.

Back to top