Responsive modular typography scales in css
Be you, a graphic designer in the 1800s making some dope _Wanted _poster for the town vagrant. You ainât got DaFonts, dawgâcanât download thatâso you gotta order some type from the foundry.
You open up their last catalog and see a whole mess of sizes: 6 ⢠8 â˘10 ⢠12 ⢠14 ⢠16 ⢠18 ⢠21 ⢠24 ⢠36 ⢠48 ⢠60 ⢠72. âThatâs right nice of âem,â your 19th century-self says. âAt least I donât gotta decide between 100 numbers.â
Fast-forward to today, where 21st-century you is making a modern-day Wanted poster called a web
site. Only this time, you can pick out as many fonts and sizes as you want*.* So you start out
simple. 16px
. 12px
. 13px
. Then you realize you have to add responsive styles, so you add in
some media queries and throw more 15px
, 18px
, and even some 24px
at it. Then you read a blog
article about relative type sizes, and suddenly youâre in em
-land. 1em
. 0.75em
. 0.875em
. No,
waitâ0.873em
â that 5 on the end felt a little too big so you nudged it down.
âOh, I forgot about responsive styles again,â you say.
For many, picking type sizes is a dark artâa âwhat feels rightâ approach. Deciding whatâs big and whatâs small on the page is the easy part; itâs the how big and how small that requires thought. Introducing typographic scales: a set-it-and-forget-it approach to managing font sizes.
A typographic scale is a small list of fixed sizes: 10 ⢠12 ⢠14 ⢠16 ⢠18 ⢠21 ⢠24 ⢠36 âŚ. Any list of sizes can make up a type scale, but a good type scale is flexible enough to fit the design, without allowing too many options that result in choice overload and inconsistency.
Responsive design is about relationships
A type scale is fixed numbers, sure. But how does a type scale apply to responsive design? It does so in relationships.
For a practical example of this done well, letâs use something convenient: the very article youâre
reading! On mobile screens, Mediumâs heading and subheading font sizes are 28px
and 22px
,
respectively. On desktop, they all increase to 32px
and 24px
. When one font size increases, the
surrounding font sizes must, too, so that those typographic relationships are all preserved.
Letâs turn this into a type scale: 22 ⢠24 ⢠28 ⢠32. If a subheading is 22, and heading is 28 on mobile, that means theyâre 2 steps apart on that type scale (start at 22, and go up two steps to arrive at 28). On desktop, both bump up one to become 24 and 32. How far apart are they? Still two steps apart on the type scale. Thatâs their relationship: one is two steps up from the other.
When it comes to responsive design where size changes across devices, keeping track of only a couple typographic relationships isnât hard. But when there become thousands of relationships in a large design system, it becomes impossible unless you have a system to manage it.
Type Scales in CSS
Full credit for this approach goes to Spencer Mortensen in this blog post on typographic scales. This post builds on his research and applies it in a more practical, modern front-end setup.
Consider the following CSS (using variables, which is now supported in every major browser):
:root {
--step-up-5: 2em;
--step-up-4: 1.7511em;
--step-up-3: 1.5157em;
--step-up-2: 1.3195em;
--step-up-1: 1.1487em;
/* baseline: 1em */
--step-down-1: 0.8706em;
--step-down-2: 0.7579em;
--step-down-3: 0.6599em;
--step-down-4: 0.5745em;
--step-down-5: 0.5em;
}
This is based on the pentatonic scale from Spencerâs blog post. In other words, every 5 âstepsâ up on the scale, the type size doubles. Every 5 steps down, the type size cuts in half.
_But __em_
s are so hard to use because theyâre relative! Youâre probably thinking. And youâre
rightâthereâs a lot of math you have to do when youâre nesting em
s inside em
s.
Unless youâre using a special relative scale like the one above. Believe it or not, you can nest the numbers above infinitely, and still stick to the same type scale.
Donât believe me? Try putting a font-size: var(--step-up-1)
inside another element with
font-size: var(--step-down-1)
. 1â1 = 0, right? Letâs look at what happens when we multiply
1.1487em
Ă 0.8706em
(--step-up-1
Ă--step-down-1
): 1em
. In other words, 100%. Right back
where we started. Multiply any of the steps togetherâ --step-up-2
and --step-down-2
, or even
--step-up-4
and --step-``down``-3
âand youâll end up with the same step as if you added the
numbers together.
Even if you didnât follow all the math, just know that you can nest this responsive type scale infinitely, and itâll still always be the same scale.
Say you had this structure:
<h1>
Iâm a heading!
<small>Iâm a normal text size</small>
</h1>
If you wanted the <small>;
to be 1em
, but itâs in a header which needs to be 2 steps up the
typographic scale, all youâd need is the following:
h1 {
font-size: var(--step-up-2);
}
small {
font-size: var(--step-down-2);
}
All you have to worry about is going up, or going down _the ladder. And let the math figure itself out. Essentially, no matter how deeply-nested your elements are, _theyâll still stick to the same scale.
Component Design â¤ď¸s Relative Type Scales
Approaching this from a component design oriented perspective where our user interface is made of reusable componentsâyou know: cards, modals, headers, footers, scrollspysâŚâis the perfect fit for this kind of typographic scale.
For example, if you have a .card
:
.card {
font-size: 16px;
}
You can either set the base font sizing to 1rem
(default size), or declare 16px
or some other
absolute value. From there, use things like:
.card-heading {
font-size: var(--step-up-2);
}
.card-subheading {
font-size: var(--step-down-1);
}
.card-body {
font-size: 1em;
}
.card-footnote {
font-size: var(--step-down-2);
}
And youâll have a wonderfully self-contained typographic system on each component. Then, if you need to adjust the base size responsively, itâs a cinch:
.card {
font-size: 18px; /* Bigger on mobile */
@media (min-width: 600px) {
font-size: 16px; /* Smaller on tablet & desktop */
}
@media (min-width: 1800px) {
font-size: 18px; /* Big again on huge displays */
}
}
Note: nested selectors are currently unsupported without something like cssnext.
In this process, you reduced managing 12 font sizes (4 sizes over 3 breakpoints) into 1 font size per breakpoint. The amount of time youâll spend managing responsive sizes is reduced significantly across the app, and your font sizes are all consistent, to boot!
This operates off a simple but basic theory: your app should respond and rearrange based off content and devices sizes, but your typographic relationships should not.
A note on different fonts
It probably goes without saying that the relationships afforded by a typographic scale only apply to scaling the same font up or down. Comparing one typeface to another is usually an apples-to-oranges situation, and technically speaking, a proper typographer would recommend you develop one scale per typeface.
Iâll confess Iâm lazy, and I typically use 1 type scale even on a multi-font website. Itâs just simpler, and the converse of setting font family ratios to one another is a hurdle I havenât found a sane way of tackling without mixins or some other method that becomes a hot mess down the line.
Regardless, this article is just a primer to type scales, and I do encourage you to employ as many type scales as your design needs, so long as it improves the typography and doesnât put you into developer debt in the future.
Building your own type scale
These examples used the pentatonic scale, but maybe youâve tried it and decided it wasnât delicate enough for your app. Hereâs how to calculate your own typographic scale. You only have to calculate this once at the start of your project, and from there itâs plug-and-play.
Tip: if youâre on a Mac, you can calculate these quickly with Spotlight (â+Space)
Pentatonic Scale (doubles every fifth step)
Enter in the formulas to the left of the arrow in a calculator (_2^(5/5)_
) to get the results
on the right-hand side.
2^(5/5) -> 2
2^(4/5) -> 1.7411
2^(3/5) -> 1.51572
2^(2/5) -> 1.31951
2^(1/5) -> 1.14869
2^(-1/5) -> 0.87055
2^(-2/5) -> 0.75785
2^(-3/5) -> 0.65975
2^(-4/5) -> 0.57435
2^(-5/5) -> 0.5
Golden Ditonic Scale (every other step, increases by the golden ratio, 1.618)
1.618^(3/2) -> 2.0581
1.618^(2/2) -> 1.618
1.618^(1/2) -> 1.272
1.618^(-1/2) -> 0.7861
1.618^(-2/2) -> 0.618
1.618^(-3/2) -> 0.4856
The very, very subtle scale (doubles every eight steps)
2^(8/8) -> 2
2^(7/8) -> 1.834
2^(6/8) -> 1.68179
2^(5/8) -> 1.54221
2^(4/8) -> 1.41421
2^(3/8) -> 1.29684
2^(2/8) -> 1.18920
2^(1/8) -> 1.0905
2^(-1/8) -> 0.917
2^(-2/8) -> 0.8409
2^(-3/8) -> 0.7711
2^(-4/8) -> 0.7071
2^(-5/8) -> 0.64841
2^(-6/8) -> 0.5946
2^(-7/8) -> 0.54525
2^(-8/8) -> 0.5
Common Formula
Using the above examples, by now youâve probably gleaned the common formula:
multiplier ^ (step / interval)
Using this, you can calculate all your values and put them into CSS or Sass variables (I use
--step-up-1
, --step-up-2
, --step-down-1
, --step-down-2
, etc. because itâs easy for me to
remember, but you can name these whatever you want!). You can even extend the existing scales above
to go as far as you want in either direction.
A warning against formulatizing this
Long-time Sass users might get the idea to turn the above into a formula (as I initially did).
However, Iâd advise against that, and instead store these em
values as hard variables like in the
examples. The reason is: if you change your type scale, youâll have to update every component in
your app (which I also did, and regretted, and in response removed that formula from my app so no
one else could change it).
Changing the typographic scale changes every typographic relationship in your app, which is the equivalent of a redesign or design refresh. Everything will feel âoffâ if you built it on one scale, but switched to another midway. Thatâs not the intent of typographic scales.
I prefer to have these values be statically-entered, as a reminder that this isnât a mere âsetting;â itâs hard-baked into the design.