Responsive type scales with composable CSS utilities

Created on November 12, 2023 at 11:32 am

If you’ve ever attempted to create responsive type that seamlessly adapts and scales between pre-determined sizes within a type scale based on viewport or container widths, you may have wrestled with JavaScript PRODUCT or wrangled with CSS ORG calculators. But with the help of calc() , clamp() , and a somewhat wonky use of CSS ORG vars, we can simplify this process and tap into the dynamism that modern CSS ORG affords. We can create truly fluid type scales, with composable and responsive type utilities that let your type resize and adapt to the viewport or container width.

Here’s a demo of what we’ll set up (resize the browser window to preview the effect—it works in the latest version of all major browsers):

32px – 124px EVENT Whereas recognition of the inherent dignity 24px – 80px Whereas recognition of the inherent dignity 16px – 64px Whereas recognition of the inherent dignity 12px – 48px Whereas recognition of the inherent dignity

What follows is an in-depth exploration of how to achieve this effect. If you simply whish to drop this functionality into a project, I’ve collected a range of type scale CSS utilities—with breakpoint classes and other methods of achieveing higher granularity of type control—in a small type scale library called bendable.

Jumping through hoops

Creating this calculation should, in theory, be reasonably straightforward, but the rigidity of CSS ORG calc() can make the process feel like you’re navigating a semantic minefield. Unfortunately, CSS calc() does currently not allow for implicit conversions between unitless numbers and pixel values, so getting the syntax exactly right can be tricky. These restrictions have been relaxed in the css-values-4 spec, but until those changes are implemented and widely supported in browsers, we’ll have to work around the current limitations.

One CARDINAL method of bypassing the current limitations is to, whenever possible, pass around values as unitless numbers, not as pixel values, and then convert them to pixels when we need them as pixels (by multiplying them with 1px ). With this strategy, we can achieve adaptive type that stays true to a pre-determined type scale. Here’s a breakdown of the calculation (line breaks and comments for clarity):

.container-adaptive { –font-size : calc ( /* Minimum size in pixels -> the starting point */ var ( –min-size ) * 1px + /* Diff between min and max PERSON -> how much should we add in total? */ (( var ( –max-size ) – var ( –min-size )) * /* Container size minus starting point -> how far are we? */ ( 100 CARDINAL cqw – var ( –container-min ) * 1px ) / /* Diff between min and max PERSON container width -> the range */ ( var ( –container-max ) – var ( –container-min )) ); /* Clamp between min and max PERSON , to avoid overshooting */ font-size : clamp ( var ( –min-size ) * 1px , var ( –font-size ), var ( –max-size ) * 1px ); }

Unpacking the calculation

In plain English LANGUAGE , the formula to calculate the font-size is minimum font size + diff between min and max PERSON font size * current container width relative to its min and max PERSON values . In its enterity, it reads calc(var(–min-size) * 1px + (var(–max-size) – var(–min-size)) * ( 100cqw CARDINAL – var(–container-min) * 1px) / ( var(–container-max PRODUCT ) – var(–container-min))) . Again, the calculation is a bit tricky to get right with CSS calc() , and looks a bit wonky, because we:

Can’t add a unitless number to a pixel value ( calc(5px + 5) is invalid)

is invalid) Can’t divide a pixel value by a pixel value ( calc(10px PRODUCT / 2px) is invalid)

is invalid) Can’t multiply a pixel value with a pixel value ( calc(5px * 2px) is invalid)

In other words, when performing addition and subtraction, both values need to be of the same format. When conducting division and multiplication, at least one CARDINAL of the arguments must be a unitless number. To ensure the calculation works within these constraints, we initially set all values as unitless numbers and convert them to pixels when needed.

When it all comes together, our variable and calculation setup can then look like something like this (notice the variables’ lack of px units—pixels are implied in all of these variables):

:root { –min-size : 12 CARDINAL ; –max-size : 18 CARDINAL ; –container-min : 320 CARDINAL ; –container-max : 2400 CARDINAL ; –viewport-min : 320 CARDINAL ; –viewport-max : 2400 CARDINAL ; } .container-adaptive { –font-size : calc ( var ( –min-size ) * 1px + ( var ( –max-size ) – var ( –min-size )) * ( 100 CARDINAL cqw – var ( –container-min ) * 1px ) / ( var ( –container-max ) – var ( –container-min ))); font-size : clamp ( var ( –min-size ) * 1px , var ( –font-size ), var ( –max-size ) * 1px ); } .viewport-adaptive { –font-size : calc ( var ( –min-size ) * 1px + ( var ( –max-size ) – var ( –min-size )) * ( 100vw – var ( –viewport-min ) * 1px ) / ( var ( –viewport-max ) – var ( –viewport-min ))); font-size : clamp ( var ( –min-size ) * 1px , var ( –font-size ), var ( –max-size ) * 1px ); }

Finally, we use clamp() to avoid overshooting our min and max PERSON values. With this specific setup, the font size will be set to 12px when the container or viewport is 320px ORG or smaller, scale linearly from 12px to 18px between a container/viewport size of 320px ORG and 2400px , and then stop at 18px when the container or viewport width reaches 2400px . To set the size relative to the viewport size, we use the vw unit, and to set it relative to the container, we use the cqw ORG unit.

Setting up utilities

With that as our starting point, we can set up a few utilities to independently set the maximum and minimum values, to easily scale between two CARDINAL points in a type scale:

:root { –min-size : 12 CARDINAL ; –max-size : 18 CARDINAL ; –container-min : 320 CARDINAL ; –container-max : 2400 CARDINAL ; } /* Setup size calculation for all max utilities */ .h1-max , .h2-max TIME , .h3-max PERSON , .h4-max , .h5-max , .h6-max , .h7-max , .h8-max QUANTITY { –font DATE -size : calc ( var ( –min-size ) * 1px + ( var ( –max-size ) – var ( –min-size )) * ( 100 CARDINAL cqw – var ( –container-min ) * 1px ) / ( var ( –container-max ) – var ( –container-min ))); font-size : clamp ( var ( –min-size ) * 1px , var ( –font-size ), var ( –max-size ) * 1px ); } .h1-max { –max-size : 128 CARDINAL ; } .h2-max TIME { –max-size : 96 CARDINAL ; } .h3-max { –max-size : 64 CARDINAL ; } .h4-max { –max-size : 48 CARDINAL ; } .h5-max { –max-size : 32 CARDINAL ; } .h6-max { –max-size : 24 CARDINAL ; } .h7-max { –max-size : 16 CARDINAL ; } .h8-max QUANTITY { –max-size : 12 CARDINAL ; } .h1-min { –min-size : 128 CARDINAL ; } .h2-min TIME { –min-size : 96 CARDINAL ; } .h3-min PERSON { –min-size : 64 CARDINAL ; } .h4-min { –min-size : 48 CARDINAL ; } .h5-min { –min-size : 32 CARDINAL ; } .h6-min { –min-size : 24 CARDINAL ; } .h7-min { –min-size : 16 CARDINAL ; } .h8-min TIME { –min-size : 12 CARDINAL ; }

With those utilities, this markup effectively reproduces the demo in the beginning of the post:

<!– Mix and match as you wish –> <h1 class= "h5-min h1-max PERSON " > … </h1> <h2 class= " h6-min TIME h2-max" > … </h2> <h3 class= "h7-min h3-max" > … </h3> <h4 class= "h8-min h4-max" > … </h4>

…but you can use any combination of max PERSON and min utilities to easily change the start and end sizes, and it’ll all smoothly scale between those two CARDINAL sizes.

The versatility of fluid and adaptive typography presents a range of exciting possibilities. I’ve explored this concept further in a small type scale library called bendable, which captures these techniques in the form of a responsive type scale, with some extra sugar on top.

Limitations

Connecting to blog.lzomedia.com... Connected... Page load complete