Digging in to dynamic typography

Jason Pamental
Aug 12, 2019 · 11 min read

The core idea of responsive typography is that the main elements of typographic structure and hierarchy should scale based on viewport size. Small screens can have more subtle size or weight distinctions and still convey hierarchy. Reading comfort is best served by keeping font size, line-height, and line length in sync: tighter line-height (leading) for narrower columns; more open line-height spacing as the line length gets longer on bigger screens. It also aids legibility to modulate font-weight for headings as font size changes. The smaller the physical size, the more you might want to reduce the weight in order to maintain legibility (reducing weight from 700 to 625 on headings for example). It can also be useful (if it’s available) to slightly reduce the width of the text on smaller screens to fit a few more characters per line. This is especially useful with larger heading sizes.

Previously, I’ve always recommended using a series of breakpoints to adjust each of these parameters as the screen size changes. But this require a lot of code, and can be hard to maintain and keep in sync across different HTML elements.

Table showing typographic scale by element and screen size
Table showing typographic scale by element and screen size
This table is taken from my 2014 book Responsive Typography. It shows scaling various elements based on device size using media queries

As I showed last week and have been demonstrating in talks over the past year or so, new CSS techniques are opening up possibilities for greater fluidity with less code. Variable fonts let us do even more. This technique uses calculations in CSS (and augmented with JS) to scale elements such as font-size, line-height, font-weight, and font-stretch (width) fluidly from a supplied lower value to a higher one, starting at some breakpoint and capping the scale at a larger one. This prevents the text from ever getting smaller or lighter or narrower than the lower value, or bigger than the higher one.

Demo page on large screen and small, showing the scale in size, width, and weight
Demo page on large screen and small, showing the scale in size, width, and weight
Our demo page on large screen and small. Note the scale in size, width, and weight

This time I’ve eliminated all of the extra code and focused solely on the aspects of scaling the typography (you can follow along on CodePen). We can build on this in the future to layer in font loading, fallback font tuning, static web font support with the variable fonts scoped within an @supports block, and more complete abstraction to use the same CSS and JS with different font stacks and size ranges. But this is a bit more constrained. For the curious, I’m also featuring another of Monotype’s upcoming releases, Frutiger Neue Variable. It’s a real beauty, and the width and weight axes give a huge amount of range to a real workhorse of a typeface.

Dynamic typography demo on CodePen, featuring Frutiger Neue Variable from Monotype
Dynamic typography demo on CodePen, featuring Frutiger Neue Variable from Monotype
Dynamic typography demo on CodePen, featuring Frutiger Neue Variable from Monotype

Last week’s example included scaling font-size and line-height with calculations, but font-weight and font-stretch were set statically for small, medium, and large values — spread across breakpoints. The reason for that is by their nature, CSS calculations like these have a unit value once the calculation is completed. That works for font-size and line-height, but not for font-wight (needs to be just a number) or font-stretch (which must be expressed as a percentage). While the CSS specification is updating to help you arrive at a unit-less value from a calculation, that capability does not yet exist in browsers. So this week I’ve added some JavaScript that I adapted from Mandy Michael’s Fluid Font Weight demo on CodePen.

The adaptations were made to allow us to feed in multiple sets of variables to scale values for font-weight and font-stretch in the same manner as font-size and line-height. Since we set the low and high end values in our element declarations, we don’t have to worry about the JS not working — but when it does, those values will scale as smoothly as everything else.

The setup

:root {  /* breakpoint variables */
--bp-small: 25;
--bp-medium: 45;
--bp-large: 55;
--bp-xlarge: 65;
/* set px value sizes */
/* initial px value for text */
--rem-px: 16;
/* EDIT HERE to set px value sizes */
...
--h1-size-s-px: 40;
--h1-size-l-px: 72;

/* calcs to create values for the formulas */
--h1-size-min: calc( var(--h1-size-s-px) / var(--rem-px) );
--h1-size-max: calc( var(--h1-size-l-px) / var(--rem-px) );
--h1-lh-min: 1.05;
--h1-lh-max: 1.1;

/* values to scale across breakpoint range */
--h1-wdth-min: 75;
--h1-wdth-max: 85;
--h1-wght-min: 700;
--h1-wght-max: 225;
/* default starting scale value */
--h1-wdth: var(--h1-wdth-min);
--h1-wght: var(--h1-wght-min);
}

Walking down through the code above, we first set breakpoint variables in ‘em’ units, but just as numerals. That’s because we’ll need to do some math with them, so we’ll add the ‘em’ when we need it. Then you’ll see a value for the base text size (we’re setting things up with 16 as the base pixel equivalent). Next we set the pixel values for the low and high end of the size range for the ‘h1’ element. Now we combine them to end up with ‘em’ values to use in our formula, low and high values for line height scaling, width, and weight. Finally, we set the low values as the default values to start with. This last bit is important: these two custom property assignments are the ones we’ll be dynamically reassigning as the viewport changes with our JavaScript.

Starting small(screen)

h1 {
font-family: var(--font-stack-heading);
/* set low-end scale values for smallest screens */
font-size: calc( var(--h1-size-min) * 1rem);
line-height: var(--h1-lh-min);
/* JS will scale the next two values dynamically */
font-stretch: calc( var(--h1-wdth) * 1%);
font-weight: var(--h1-wght);
}

Sliding into scale

h1 {
font-size:
calc(( var(--h1-size-min) * 1em ) +
( var(--h1-size-max) - var(--h1-size-min) ) *
((100vw - ( var(--bp-small) * 1em )) /
( var(--bp-xlarge) - var(--bp-small) )));
}

So let’s restate that another way:

Please scale the font size from [this] to [that], starting when the screen width reaches [here] and stop when it gets wider than [there]

In this case, as we showed above, we want to scale from an equivalent of 40px to 72px, starting when the viewport reaches 25em, and stop scaling when the viewport is wider than 65em. The magic is where we bring viewport units into the formula. The other parts are fairly straightforward. But browsers can do math that we cannot — so that’s how we get two important outcomes: the value scales smoothly, but because we’ve mixed viewport units with ems, users can still zoom in on the text. When using strictly viewport units for sizing, the text will never scale — creating a huge accessibility issue.

The nice thing is all we have to do is swap out the variables for font-size (or line-height) and we can use exactly the same structure everywhere we want to use it. The only caveat at this point being CSS does not support ending up with a unit-less value. This is why we can only use this technique for things like font-size or line-height. To scale font-weight (which needs just a number) or font-stretch (which must be expressed as a percentage), we need a different solution.

Just a little slide to the left

Demo content without width or weight scale
Demo content without width or weight scale
There’s nothing inherently wrong with having the width and weight stay the same, but there’s opportunity for improvement when we scale them to best suit the device and context
Demo content with scaled weight and width for both headings and body copy
Demo content with scaled weight and width for both headings and body copy
Now with the scaling of width and weight in place for heading and body copy. Notice the weight change in the heading getting heavier on the small screen, while body text gets slightly narrower

Primarily the changes revolve around reading and setting more CSS custom property values for breakpoint settings, minimums, and maximums — and do that for multiple HTML elements. In this case we want to scale the width and weight of h1’s, h2’s, and p’s. Better JS wranglers than I could probably streamline this even more, but it’ll do for now!

First we set up the functions (one main one, one nested inside it), and then the event listeners (on load, and on resize). This ensures we get correct initial values, and keep them correct whenever the viewport changes.

function variableResize() {  ...
// set up values by reading existing CSS properties
...

function scale(varName, minValue, maxValue) {

...
// scale the value, and write it back to the CSS property
...
}
}
window.addEventListener("load", variableResize);
window.addEventListener("resize", variableResize);

Inside the main function, first we set up the process by which we can read and save CSS custom property values. Then we read in a few values to set things like breakpoint values, and a series of variable sets for each HTML element that we want to scale. With each set of variables we call the inner function that calculates the new value and saves it back to the appropriate custom property.

function variableResize() {  // get the computed styles into an object
let bodyStyles = window.getComputedStyle(document.body);
// set up a place to save new values back
let root = document.documentElement;

// Minimum and Maximum viewport size - must be unitless em values
const maxWindowSize = bodyStyles.getPropertyValue('--bp-xlarge') * 16;
const minWindowSize = bodyStyles.getPropertyValue('--bp-small') * 16;

// Get current viewport size
const windowWidth = window.innerWidth

// Scale elements and min/max values
const h1WidthVar = '--h1-wdth';
const h1MinFontWidth = bodyStyles.getPropertyValue('--h1-wdth-min');
const h1MaxFontWidth = bodyStyles.getPropertyValue('--h1-wdth-max');
scale(h1WidthVar, h1MinFontWidth, h1MaxFontWidth);
const h1WeightVar = '--h1-wght';
const h1MinFontWeight = bodyStyles.getPropertyValue('--h1-wght-min');
const h1MaxFontWeight = bodyStyles.getPropertyValue('--h1-wght-max');
scale(h1WeightVar, h1MinFontWeight, h1MaxFontWeight);
...
function scale(varName, minValue, maxValue) {
...
// scale the value, and write it back to the CSS property
...
}
}
window.addEventListener("load", variableResize);
window.addEventListener("resize", variableResize);

The other bit of note in the inner function is that we check to see which value is greater: the ‘lower end’ or the ‘upper end’ (based on viewport range), and calculate accordingly. That way we can scale weight down while font size goes up (or whatever else we want to impact. Here’s the code:

function scale(varName, minValue, maxValue) {
// Make sure min/max are numbers
minValue = minValue * 1;
maxValue = maxValue * 1;
//Scale within a range
const percent = (windowWidth - minWindowSize) /
(maxWindowSize - minWindowSize);
if (maxValue < minValue) {
var valueScale = minValue - (percent * (minValue - maxValue));
} else {
var valueScale = (percent * (maxValue - minValue)) + minValue;
}

// Get the new font width
const newValue = windowWidth > maxWindowSize
? maxValue
: windowWidth < minWindowSize
? minValue
: valueScale;

// Set my CSS Custom Property for width to update.
root.style.setProperty(varName, newValue);
}

We’re only using this for width and weight — but don’t forget that you could scale any other value in a similar fashion. Just supply another set of base variable and range values.

So why do it this way

There are a couple of other solutions out there that might fit the bill: Typetura.js from Scott Kellum and Sal Hernandez, and Textblock from Theo Rosendorf and Glyphic. Typetura in particular is interesting as it has a UI to prototype the typography and save the resulting code. They’re actively working on improving the tool and the UI, so it’s definitely something to watch and try. Both projects rely on JavaScript to work. While this is fine in many circumstances, my goal was to use as little JS as possible, relying on native CSS for as much of the rendering process as possible. This should result in faster, smoother page rendering.

No solution is perfect, but so far this has worked under the widest array of circumstances. As noted before, this does require support of variable fonts (which also coincides with support for CSS custom properties and calculations). It can be set up (as in other demonstrations) with support for static web fonts, but that makes for a harder demo to explain. It also requires that the initial value for font-size and line-height be smaller than the end value. But that does fit with a good number of use cases. It’s maybe not so often that you want to make the type smaller as the screen gets bigger.

Ending with a beginning

Resources

This is an excerpt from my weekly newsletter about web fonts and typography. If you’d like a weekly dose of web typography tips, news, and my upcoming talks and workshops, you can sign up for the newsletter here.

If you this helpful, please share what you make — and if there’s something you’d like to see covered in a future issue, please let me know!

Originally published at rwt.io on July 8, 2019.

Web Typography News

A collection of web typography tips from my weekly…

Jason Pamental

Written by

designer, tinkerer, typographer, speaker. http://rwt.io, author:Responsive Typography (bit.ly/rwtbook). walker of @aProperCollie & @inImproperCollie

Web Typography News

A collection of web typography tips from my weekly newsletter. You can sign up for the newsletter via email at http://eepurl.com/ghd9b9 or find out more about Jason’s talks and writing at https://rwt.io

Jason Pamental

Written by

designer, tinkerer, typographer, speaker. http://rwt.io, author:Responsive Typography (bit.ly/rwtbook). walker of @aProperCollie & @inImproperCollie

Web Typography News

A collection of web typography tips from my weekly newsletter. You can sign up for the newsletter via email at http://eepurl.com/ghd9b9 or find out more about Jason’s talks and writing at https://rwt.io

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store