Responsive type scales with composable CSS utilities

By admin
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