Published

Fluid type sizes and spacing

I’ve been using a fluid type and spacing system on the most recent builds I’ve completed. Here’s why I use it, and how I approach it. I mainly use SCSS (a Sass syntax), but it’s also very do-able with plain CSS.

Screencast of Gort Scott’s homepage, resizing it in Chrome’s inspector

The example above demonstrates the result on gortscott.com, resizing the window from about 2300px down to about 640px and back again. The type and spacing across the page begins scaling down when the window is 2095px wide and stops shrinking at 1047px wide. At that point the text begins to reflow as the CSS Grid layout continues to shrink. Eventually at 703px wide the layout shifts, and again at 543px wide.

Why fluid type?

Designers will often come to me with desktop layouts only, or sometimes desktop and a few mobile layouts, and we then collaborate to “fill in the blanks” in terms of what should happen to the typography and spacing at other screen widths.

In the past, I used breakpoint-specific type sizes. This worked but was never really satisfying. It felt like something was lost at the intermediate or larger screen sizes, the proportions were all wrong. The jumps between sizes and spacing felt jarring. Also, the likelihood that someone might be looking at a site I’ve built on an “intermediate” screen is pretty high, there are so many devices now with screen sizes somewhere between a phone and a laptop. I don’t want my sites to look wonky on an iPad!

I decided to try fluid type and spacing with the Gort Scott website, a collaboration with Polimekanos deployed in early 2021, and was pleased with the result, particularly how the more complex layouts on the project pages were able to scale at the intermediate sizes. The proportions were preserved.

I implemented this on the Alison Jacques website next, ultimately deployed around mid-2021, and also on a recent typography update to the Modern Art website that had originally launched back in 2018. Both the Alison Jacques and Modern Art websites were collaborations with John Morgan studio.

To get a feel for how fluid type and spacing feels, visit any of those three sites and resize your window to see how the type and spacing scales with the viewport. These specific pages are particularly relevant examples due to the layouts and text quantity:

The Modern Art site might be tough to try if you’re on a laptop or smaller screen since it doesn’t scale up until the viewport width is over 1440px. That one is best tried on a large screen, or in the inspector.

An overview

Since I’m usually given designs at mainly one specific screen width, I start with the ideal font size values at that screen width. I then use those values to calculate the necessary minimum, maximum, and viewport width font sizes proportionately.

See the relevant SCSS below. The prefix $ft- stands for “fluid type” and keeps these variables distinct from others that might be declared in my SCSS files.

/* Sizes that the rest of our system will be based on */
$ft-screen-width: 1440;
$ft-body: 20;
$ft-body-min: 1rem;
$ft-body-max: 1.375rem;
$ft-body-vw: $ft-body / $ft-screen-width * 100vw;

/* All of the other sizes we want. The key can be any 
   string, and the value should be in pixels without 
   the unit */
$ft-sizes: (
  h1: 30,
  h2: 26,
  h3: 20,
  h4: 16,
  small: 12
);

/* Mixin to generate the the fluid sizes as 
   custom properties */
@mixin sizeVar($name, $size) {
  $proportion: $size / $ft-body;
  $min: $ft-body-min * $proportion;
  $max: $ft-body-max * $proportion;
  $vw: $ft-body-vw * $proportion;
  --ft-size-#{$name}: clamp(#{$min}, #{$vw}, #{$max});
}

/* Declare the body font size custom property, then use the 
   mixin to generate all of the other custom properties */
:root {
  --ft-size-body: clamp(#{$ft-body-min}, #{$ft-body-vw}, #{$ft-body-max});
  @each $name, $size in $ft-sizes {
    @include sizeVar($name, $size);
  }
}

You can also find a more full-bodied example with SCSS and HTML on CodePen. That example is closer to how I actually use it, with @mixins for different styles. Alternatively, check out a vanilla CSS approach later in this post.

Breaking it down

First, we establish the ideal body copy font size in pixels at a specific window width, the minimum body copy size in rem, the maximum body copy size in rem, and the body copy size in vw units so that it will scale as the window is resized.

$ft-screen-width: 1440;
$ft-body: 20;
$ft-body-min: 1rem;
$ft-body-max: 1.375rem;
$ft-body-vw: $ft-body / $ft-screen-width * 100vw;

Here’s what each SCSS variable stands for:

  • $ft-screen-width: 1440; means that we’re working with a screen width of 1440px as our “base”
  • $ft-body: 20; means that we want the body font size to be 20px at the screen width established in $ft-screen-width
  • $ft-body-min: 1rem; means that we want the body font size to shrink to 1rem (16px) at minimum
  • $ft-body-max: 1.375rem; means that we ant the body font size to grow to 1.375rem (22px) at maximum
  • $ft-body-vw calculates the appropriate font size in vw units by dividing the $ft-body by the $ft-screen-width value to get a proportion, and then multiplying it by 100vw so that we have the proportion in vw units

We’ll eventually combine the minimum, maximum, and vw values in a custom CSS property to give us our scaling font size.

⚠️ Accessibility note: it’s critical to use rem units for the minimum and maximum because these are relative units. This allows users to resize the type for readability, unlike px or vw units.

Next, we set up a Sass map (basically an array) that includes all of the other font sizes we need.

$ft-sizes: (
  h1: 30,
  h2: 26,
  h3: 20,
  h4: 16,
  small: 12
);

This map can contain as many key/value pairs as you want. The key name can be any string (no spaces though, since it will become part of a custom CSS property), and the value should be the correct font size in pixels at the screen width we established previously. The value should not include the px unit, just the number.

Now we set up the Sass @mixin we’ll use to generate each of the custom CSS properties.

@mixin sizeVar($name, $size) {
  $proportion: $size / $ft-body;
  $min: $ft-body-min * $proportion;
  $max: $ft-body-max * $proportion;
  $vw: $ft-body-vw * $proportion;
  --ft-size-#{$name}: clamp(#{$min}, #{$vw}, #{$max});
}

The first argument $name is the size name that will ultimately be used in the custom property name. The second argument $size is the desired font size at the screen width we previously established in $ft-screen-width.

In the @mixin, we first divide the desired font size $size by the body font size $ft-body to get the proportion $proportion of this new font size to the body font size. And then we use that proportion to calculate the appropriate minimum, maximum, and vw font sizes as proportions of their body counterparts.

Once we have all of the necessary sizes, the @mixin spits it all out as a custom property and CSS variable using the clamp() CSS function to restrict the scaling to our minimum and maximum values. (Note that if you inspect the example sites linked early on in this post, you’ll find nested min() and max() functions. That works too! But the syntax is more confusing.)

Finally, we establish all of our custom CSS properties in the :root.

:root {
  --ft-size-body: clamp(#{$ft-body-min}, #{$ft-body-vw}, #{$ft-body-max});
  @each $name, $size in $ft-sizes {
    @include sizeVar($name, $size);
  }
}

First we set up the --ft-size-body custom property since we can’t use the @mixin to generate the CSS variable. Then we use a Sass @each rule to go through each of the sizes from the $ft-sizes map and @include the sizeVar() @mixin to generate our custom properties and CSS variables.

We can then use those custom properties throughout our styles as necessary, for example:

/* Simple example of fluid type custom properties in use */
h1 {
  font-size: var(--ft-size-h1);
}

h2 {
  font-size: var(--ft-size-h2);
}

cite {
  font-size: var(--ft-size-small);
}

p, ol, ul {
  font-size: var(--ft-size-body);
}

And so on.

The gotchas

There are a few things you need to be quite careful about when working with this system.

Using em units for spacing

For the spacing, using em units keeps everything in proportion with the type as it scales. The problem with using em units is that they’re a bit tricker to wrap your head around. 1em in one place might mean 16px, whereas in another place it might mean a completely different size.

This isn’t terribly problematic, you can just do all of the necessary calculations and get the correct em values in place. But it gets dicey if/when the spacing needs to be tweaked based on designer or client feedback since you then have to recalculate all of these values in so many places.

I haven’t come across any perfect fix for this. The most manageable approach I’ve found is to usually apply all non-body font styles directly to the child-most block-level element (e.g. p, ul, etc.), to apply the body font styles directly to the body element, and to apply as much spacing as possible to ancestor elements. This way, 1em = 1rem (effectively) most of the time, and you have just a few deviations where necessary in places like the margin-bottom of a heading.

Sass mixins are a useful way to group font styles that can be reused wherever necessary. The SCSS example I put together in CodePen demonstrates this a bit.

Catch major changes as early as you can

Once the system is in place, it can be tricky to make major adjustments because all of the proportions are based on one another. It’s like cutting a single loop of yarn in a sweater, you have to knit it back together carefully to avoid it unravelling.

This makes the early prototyping phase all the more important so that spacing tweaks and that sort of thing are caught when you’re establishing the system as opposed to later when you’ve rolled out that system across a whole website.

I’ve found that it’s usually best to put together a basic component library that demonstrates the basic font sizes you’ll be using at the “ideal” screen size (the one established in $ft-screen-width from earlier in this article) and work out the kinks there first. Once that is okayed, then you can move it all in to a system such as the one described here.

Outliers

Though this system works nicely to keep everything in proportion regardless of the screen size, there may be situations where you want to break out of those proportions on the minimum or maximum ends.

It might be worth altering the @mixin in that case to include optional $min and $max arguments. Or you could just forgo the @mixin and @each entirely and be a bit more repetitive with your code. No shame in being less DRY if it makes sense for your use case.

Browser quirks and support

Until recently (not sure how recently…), Safari didn’t support this sort of font scaling when the browser window was resized. If you implemented this and then tried to resize the window, it wouldn’t budge. It would only change once you refreshed the page. A hacky way around this was to alter the z-index of the body element on resize using JavaScript (throttled, ideally, so that it doesn’t happen on literally every pixel change!).

I wanted to mention this since I had to do it for the sites I mentioned at the beginning of this post but based on the CodePens I put together recently, I don’t think that is necessary any longer.

And of course, this whole system relies pretty heavily on more modern CSS elements like custom properties and clamp(). The browser support is pretty good for these features, so it’s not too big a worry unless you have to support something like Internet Explorer. God help you…

Using plain CSS

To be clear, you could absolutely do something similar with plain ol’ CSS. It would look something like this:

:root {
  /* Sizes that the rest of our system will be based on */
  --ft-screen-width: 1440;
  --ft-body: 20;
  --ft-body-min: 1rem;
  --ft-body-max: 1.375rem;
  --ft-body-vw: calc(var(--ft-body) / var(--ft-screen-width) * 100vw);
  
  /* Body, 20px on 1440px screens */
  --ft-size-body: clamp(var(--ft-body-min), var(--ft-body-vw), var(--ft-body-max));

  /* H1, 26px on 1440px screens */
  --ft-h1: 26;
  --ft-size-h1: clamp(var(--ft-body-min) * (var(--ft-h1) / var(--ft-body)), var(--ft-body-vw) * (var(--ft-h1) / var(--ft-body)), var(--ft-body-max) * (var(--ft-h1) / var(--ft-body)));

  /* And so on with H2, H3, etc. */
}

⭐ Side note: you don’t have to use calc() within clamp()!

If you went with the CSS example above, you’d have to repeat lines 13–14 for each of your non-body font sizes, changing the custom property name and the font size in pixels for each one. I prefer SCSS since there’s less repetition, and the clamp() statement isn’t so long, but this does work! See example on CodePen.


The catalyst for finally writing this all up was Utopia.fyi, the fluid type system shared by the folks at ClearLeft, specifically James Gilyead and Trys Mudford.

See also Typetura, though keep in mind that Typetura seems to do a lot more than I am doing here! It incorporates much more nuanced typographic considerations than can realistically be considered in a more simple system like the one I’ve developed.

I’m 100% sure that lots of other people have done something similar. If you have and went about it a different way, or have suggestions on how to improve this, please share!