How to handle responsive font sizes in css
Are you making typography on the web harder on yourself than it needs to be? Probably. What if I told you I could give you a typographic scale that is:
- Universal: works in CSS, Sass, and CSS-in-JS
- Simple: easy to set up, quick to learn
- Stylish: actually looks good (it follows typographic principles)
- Automatically-responsive: makes responsive text sizes easier than youâre used to
- Adjustable you can actually tweak it to taste
Would you not use it? Well good news, friends, it does exist! And itâs called Responsive Type Scales. And I donât know for the life of me why itâs not the norm. Maybe it needs a more buzzworthy name. Maybe it needs a landing page (đ¤ actually it probably does).
This blog post will cover:
- Responsive Type Scales: the basics
- Responsive Type Scales: the theory
- tips / faqs
âBut is this for me?â Youâll ask. Yes. Yes, this is for you. If you type words and put them on the internet, you should use this. You can check the âFAQâ section at the end to learn how it works with special edge cases and such.
part 1: responsive type scales
getting started
Typographic scales are a Goldilocks solution to font sizes. We donât want too many font sizes in our site, but we need just enough to have the design look good and have structural hierarchy. We need just the right amount.
If youâve ever used âshirt sizeâ names like font-s
, font-m
, and font-l
(small, medium, and
large, respectively), youâve likely run into the âtoo fewâ end of the spectrum (unless you resort to
dumb names like font-xxxxl
). But the real problem with this approach is this isnât responsive
(unless you resort to even dumber names like class="font-xxxl font-lg-xxxxl"
).
A responsive type scale solves all of the above: gives you just the right amount (itâs configurable, so you can set the right number you need), and handles responsive font sizes elegantly.
But before we get ahead of ourselves, youâre probably wondering âhow does it look in action?â (or you just want to copy + paste something and then leave). Letâs dive into an example first to show it in practice, then weâll step back to explain the principles and reasoning. After all, no matter how solid a concept is theoretically, will you use it if it has bad DX? Rest assured this is quick to learn, easy to use, and solves your font problems.
To get started, copy some CSS classes into your styles (donât worry; weâll explain how we generated these numbers later):
/* fonts.css */
/* (note: ignore the em values for now and only focus on the class names) */
.font-u18 { font-size: 11.390625em }
.font-u17 { font-size: 9.950627481136905em }
.font-u16 { font-size: 8.692673779389363em }
.font-u15 { font-size: 7.59375em }
.font-u14 { font-size: 6.63375165409127em }
.font-u13 { font-size: 5.795115852926242em }
.font-u12 { font-size: 5.0625em }
.font-u11 { font-size: 4.422501102727513em;}
.font-u10 { font-size: 3.8634105686174953em }
.font-u9 { font-size: 3.375em }
.font-u8 { font-size: 2.9483340684850083em }
.font-u7 { font-size: 2.575607045744997em }
.font-u6 { font-size: 2.25em }
.font-u5 { font-size: 1.9655560456566725em }
.font-u4 { font-size: 1.7170713638299977em }
.font-u3 { font-size: 1.5em }
.font-u2 { font-size: 1.3103706971044482em }
.font-u1 { font-size: 1.1447142425533319em }
.font-d1 { font-size: 0.8735804647362989em }
.font-d2 { font-size: 0.7631428283688879em }
.font-d3 { font-size: 0.6666666666666666em }
.font-d4 { font-size: 0.5823869764908659em }
.font-d5 { font-size: 0.5087618855792586em }
.font-d6 { font-size: 0.4444444444444444em }
.font-d7 { font-size: 0.3882579843272439em }
.font-d8 { font-size: 0.3391745903861724em }
.font-d9 { font-size: 0.2962962962962963em }
.font-d10 { font-size: 0.2588386562181626em }
.font-d11 { font-size: 0.22611639359078162em }
.font-d12 { font-size: 0.19753086419753085em }
.font-d13 { font-size: 0.17255910414544176em }
.font-d14 { font-size: 0.15074426239385438em }
.font-d15 { font-size: 0.13168724279835392em }
.font-d16 { font-size: 0.11503940276362785em }
.font-d17 { font-size: 0.10049617492923625em }
.font-d18 { font-size: 0.0877914951989026em }
Donât want to use CSS classes? You could use CSS variables or JS values or even Sass variablesâit is universal after all and you can translate this into whatever works best
Then just sprinkle the utilities in whenever you need it:
<h1 class="font-u6">Iâm an h1 so Iâm 6 steps up from the base size</h1>
<h2 class="font-u5">Iâm an h2 so Iâm slightly smaller than h1, but still 5 steps up from the base size</h2>
<small class="font-d1">Iâm 1 step down from the base size</small>
What if that h1
tag needs to be a little bigger? No problem; bump it up one to font-u7
. Or
smaller: font-u5
. Theyâre just numbers; you go up (font-u*
) or down (font-d*
) as you need to.
nesting
Since weâre dealing with em
s, we can nest sizes. Say we had an <h1>
tag we wanted to be big, but
inside that we have a subheading we want to be the same size as the base font. How would we
accomplish that? Easy:
<h1 class="font-u3">
Iâm 3 steps up from the base size
<small class="font-d3">Iâm the same size as the base (3 - 3)</small>
</h1>
By thinking in terms of moving âupâ or âdownâ a scale, youâre starting to think in relationships, which is how type works. Typography is less about absolute sizes, and is more about how much bigger or smaller one size is to another (how one ârelatesâ to another).
But if this nests, wonât this be a pain to keep track of when youâre several components deep? If
youâre working in components, itâs probably smart to reset the font size back to 1rem
(root font
size) so sizes within a component are predictable:
.MyComponent {
font-size: 1rem; /* reset font size back to base */
}
This may seem like an annoyance at first if you havenât done this before, but this is actually great practice. This will be an important principle for responsive sizes going forward.
responsive sizes
Responsive font sizes can be a nightmare at scale. Fortunately, using this system, you only have
to set a componentâs base sizes for each breakpoint. But to add this, letâs copy the the same CSS
values from above, but this time weâll use CSS variables and swap out the em
s for rem
s:
/* fonts.css (expanded from above) */
:root {
--font-u18r: 11.390625rem; /* the ârâ in âu18râ stands for ârootâ */
--font-u17r: 9.950627481136905rem;
--font-u16r: 8.692673779389363rem;
--font-u15r: 7.59375rem;
/* ⌠(add the rest from above) */
}
Then in your component styles, you only need to change the base size for its breakpoints:
.MyComponent {
font-size: var(--font-d1r); /* small screens: 1 step down */
}
@media (min-width: 600px) {
.MyComponent {
font-size: 1rem; /* medium screens: base size */
}
}
@media (min-width: 1200px) {
.MyComponent {
font-size: var(--font-u1r); /* large screens: 1 step up */
}
}
@media (min-width: 1800px) {
.MyComponent {
font-size: var(--font-u2r); /* extra large screens: 2 steps up */
}
}
You donât have to do anything else! Because weâre using em
s, our components all scaled while
preserving their typographic relationships. However, for the base component size, we did use rem
s
instead of em
s to blow away any nested styles we may have had to deal with otherwise
(following good principles of layout isolation).
further reading
Congratulations! Youâve just learned the whole system. đ From here you may want to explore the following:
- responsive-typography contains all the code above but packaged in a convenient npm wrapper
- CodeSandbox example showing how responsive type scales can work in an app
But perhaps even after seeing how it works, you have some lingering âwhyâ questions. Such as: how big is a âstepâ in pixels? or what if I donât like the scale above? or is this really simpler than other typographic libraries? Read on to get a little more into why this approach can work for everyone.
part 2: why responsive type scales work
Note: this section is completely optional to readâyou have everything you need to start using a responsive type scale already. Everything that follows is theory and extra information if youâd like to understand the concepts a bit deeper.
3 principles of a type scale
As we mentioned above, typographic scales solve the Goldilocks problem of font sizes: you need just enough to make the design feel complete, but not too many.
Typographic scales are so effective because theyâve had time to mature. At over 600 years old, theyâre as ancient as the printing press itself (they donât look a day over 500 though). Youâve encountered typographic scales every time youâve used a word processor like Google Docs. While youâll find some slight variation from program to program, most typographic scales are identical save for a few numbers. And they have three guiding principles that are important to keep in mind: they scale exponentially, are expressed in steps, and prioritize relative sizing over absolute.
principle 1: exponential scaling
The classic type scale you know from almost every word processor is:
6 7 8 9 10 11 12 14 16 18 21 24 30 36 48 60 72
Letâs look at the scale increase between the numbers:
6 7 8 9 10 11 12 14 16 18 21 24 30 36 48 60 72
------------------------------------------------------------------------------
1 1 1 1 1 1 2 2 2 3 3 6 6 12 12 12
Notice how the numbers increase exponentially moving up the scale (itâs not a mathematically-perfect exponential equation, but it still exponentially increases at regular intervals):
|
+--+
|
|
|
|
|
+--+
|
|
+--+
+---+
------+
The reason for this is that bigger font sizes require bigger increases for contrast. You can
probably recognize the difference between 6px
and 7px
type because thatâs a 16.6% increase in
size. But you canât tell the difference between 71px
and 72px
(1%). So 6 -> 7
make sense as a
typographic relationship, but not 71 -> 72
.
Any type scale we make has to scale exponentially, which means we can mathematically automate it! Yay!
principle 2: type scales are expressed in steps
Given type scalesâ exponential nature, it would follow that we treat them like exponents. For
example, when weâre dealing with powers of 2, how do we express it? Not as 2, 4, 8, 16âŚ
but as
2² 2Âł 2â´ âŚ
. Letâs apply that thinking to the type scale: weâll take the same scale, but number the
steps themselves. And weâll give the step itself a number (step² stepÂł stepâ´ âŚ
):
6 7 8 9 10 11 12 14 16 18 21 24 30 36 48 60 72
------------------------------------------------------------------------------
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Thinking in numbered steps lets you remove all the complexity of conditional logic. You donât have
to go âOK so Iâm at 24px
, which means the next step up is +6
but the next step down is -3
.â
You simply think in terms of being at a certain step and going up or down.
In the end, your new scale is just a list of integers, which is much simpler to reason about (this is only the bottom row from above):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Another way to think about this idea is to treat it the same way you would a music scale. You donât
say âfirst Iâll play a 440 Hz
note followed by 493.8833 Hz
.â You simply say âIâll play an A
note, followed by B.â âAâ and âBâ are just shorthand names for the tones produced that are easier to
reason about. In the same way, you donât have to remember that 9
was 16px
; you just know that
your font size is a 9
on the scale.
principle 3: relationships matter
Take a look at the following type samples:
What if I told you that the âparagraphâ size from Column B was the same value as the âheadingâ in Column A? Inspect it if you donât believe me! Our brains donât perceive absolute value as much as we think we do. Take this famous optical illusion from Edward H. Adelson:
The tiles marked âAâ and âBâ are the exact same RGB value. Our brains perceive one as lighter than the other because of their relationshipsâ âBâ is surrounded by darker colors, and âAâ by lighter colors. The illusion works because our brains are hardwired to perceive relative differences (if youâre interested in this general subject, see Interaction of Color by Josef Albers).
Managing a typographic system is the same in that the relative sizes are more important than
absolute sizes. Something isnât a heading because âheadings are 32px
and thatâs the law.â
Because as soon as your base size increases to 32px
, headings have to be proportionately bigger to
match! No, headings are only headings if theyâre noticably larger than normal text.
So how do we deal with relationships? We know that a type scale is expressed in steps already, but
now we have to think in relative steps from one size to another. Taking the scale we had before,
letâs slide the bottom numbers to center around our base font sizeâ16px
âto measure how far away
other numbers are, relatively:
6 7 8 9 10 11 12 14 16 18 21 24 30 36 48 60 72
------------------------------------------------------------------------------
-8 -7 -6 -5 -4 -3 -2 -1 0 +1 +2 +3 +4 +5 +6 +7 +8
We can easily see that 16 -> 18
is +1 step
; 16 -> 30
is +4 steps
; 16 -> 9
is -4 steps
,
etc. If we move around the scale even more, we notice more interesting relationships. 16 -> 30
is
the same âratioâ as 30 -> 72
. In both cases, there are 4 steps
in between.
So now we start to get back to those font-u4
CSS classes introduced earlier. See how no matter
what our base size started with, all that matters is applying the correct number of steps up or
down? If we just know that we want a heading to be 4 steps above our base size, it gets a
<h1 class="font-u4">
class. And if we resize everything else on the page, that typographic
relationship is preserved. That means less code to write, and less code to manage.
It takes some work, and relearning, to remap how youâve thought about font sizes on the web. But youâll be amazed at how much simpler things become when you do.
building your own scale
Say you are getting the basics of the system, but that exact scale ratio just isnât working for your fonts. This is the basic mathematical formula used, of which full credit goes to Spencer Mortensen:
đš ^ (đ / đĽ)
You have 2 basic settings to configure: đš
, the factor for multiplication, and đĽ
, the delta
(distance) between each step. Say you wanted to increase font size 2.25Ă
every 6
steps (my
personal favorite scale). That would look like this:
2.25 ^ (đ / 6)
The last variable, đ
, receives the step number youâre calculating. This is how the example back at
the beginning of the blog post was generatedâby counting up (1
, 2
, 3
, âŚ):
2.25 ^ (1/6) -> 1.1447142426
2.25 ^ (2/6) -> 1.3103706971
2.25 ^ (3/6) -> 1.5
2.25 ^ (4/6) -> 1.7170713638
2.25 ^ (5/6) -> 1.9655560457
2.25 ^ (6/6) -> 2.25
âŚ
To go down the scale, use negative numbers (-1
, -2
, -3
, âŚ):
2.25 ^ (-1/6) -> 0.8735804647
2.25 ^ (-2/6) -> 0.7631428284
2.25 ^ (-3/6) -> 0.6666666667
2.25 ^ (-4/6) -> 0.5823869765
2.25 ^ (-5/6) -> 0.5087618856
2.25 ^ (-6/6) -> 0.4444444444
âŚ
Tip: want to calculate these a bit more automatically? Try
this visual calculator or see the scale()
method in JS
in
responsive-typography
Take the above values and plug them into CSS class names, CSS variables, Sass variables, JS
variables⌠whatever your system uses. Itâs universal because at the end of the day itâs just em
s.
Now that you know how these numbers are made, you may find that the 2.25:6
scale isnât producing
the results you expected. Try playing with both numbers
and seeing what you get. Try it in an existing app! The key is experimentation.
That wraps up the main meat of the blog posts. What follows from here are tips and FAQs to try and take care of remaining questions.
appendix 1: faq
how does this compare to x, y, or z?
Ever since the idea of responsive design came about, weâve been inventing complex systems to manage typography (or weâve been ignoring it and hoping the problem goes away đ). There are too many to list out here, but prominent ones Iâve come across:
- Flexible Typography with CSS Locks:
this is probably the closest to a technically-accurate system, but just look at this
âhuman-generatedâ CSS for a single font size:
line-height: calc(1.3em + (1.5 - 1.3) * ((100vw - 21em)/(35 - 21)))
. Does your brain work like this? Mine doesnât. - Tailwind CSS: hereâs their simple example:
<p class="text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl ...">
. Imagine managing all that manually across all your markup? No thank you. - Typography.js: This requires a runtime? Iâd rather not have flickering font sizes as my pages load, thank you.
- Material UI: Decent type scale, but not responsive.
In short, other proposals have come about, but theyâre either too complicated for human-written CSS. Or they require a JS runtime for styling, which by default wonât work in a large number of setups. Iâd argue that responsive type scales strikes the best balance of simplicity and efficiency, and it works in every setup.
how can I use this with multiple fonts?
When working with multiple fonts, sometimes one scale to ârule them allâ will work just fine. But a common problem is having different fonts appear bigger or smaller than another at the same pixel size. In that case, youâll need an adjustment number. Say your site has 2 fonts: one for body, and another for headings. Say your heading font is feeling a little small when you try and use the same scale for everything:
/* fonts.css */
:root {
/* your base scale */
--font-u6: 2.25em;
--font-u5: 1.9655560456566725em;
--font-u4: 1.7170713638299977em;
--font-u3: 1.5em;
--font-u2: 1.3103706971044482em;
--font-u1: 1.1447142425533319em;
/* heading scale */
--heading-adjustment: 1.5px; /* Increase the final size of the heading font */
}
.font-heading-u6 { font-size: calc(var(--font-u6) + var(--heading-adjustment)) }
.font-heading-u5 { font-size: calc(var(--font-u5) + var(--heading-adjustment)) }
.font-heading-u4 { font-size: calc(var(--font-u4) + var(--heading-adjustment)) }
.font-heading-u3 { font-size: calc(var(--font-u3) + var(--heading-adjustment)) }
.font-heading-u2 { font-size: calc(var(--font-u2) + var(--heading-adjustment)) }
.font-heading-u1 { font-size: calc(var(--font-u1) + var(--heading-adjustment)) }
We have a --heading-adjustment
CSS variable that weâre using to modify the final output value.
Itâs being calculated at the end, so we are still using the same scale, but we can make a minor
visual tweak to make the typefaces work together better. Simply adjust --heading-adjustment
to
taste. Rinse and repeat for any other typefaces you add to your project.
can i use this in CSS-in-JS?
Yes (try the JS import from responsive-typography).
appendix 2: tips
Some various tips you may find helpful both in adopting responsive type scales, and thinking about it in the context of your larger styling system.
- If youâre struggling, try working backwards from pixels. Say youâre trying to generate your
own type scale, but you just canât find the right
factor
anddelta
that feels right. Try starting with an absolute type scale like12px 14px 16px 18px 21px âŚ
, and work backwards into the relative type scale. Use the visual calculator and look on the right hand side. How close can you get to recreating your absolute scale? Now you have more of a concrete goal to hit than simply turning dials until something âfeels right.â - Hardcode the scale if you can help it. It might be tempting to write a Sass or JS function to generate the scale values. While thereâs nothing wrong with that, know that if you change your scale, you change your design. All the hard work and perfectly-tuned styles have now just gone out the window. I like to not have this âtypographic restart buttonâ so easily-accessible on a team that may not understand the implications.
- Rename the CSS classes to whatever you like. You may not like
font-u1
orfont-d1
. Generate your own scale and come up with your own names! Keep it short and sweet, but do whatâs obvious to you. - Why not bothâuse CSS AND CSS-in-JS! It doesnât hurt to use CSS utility classes AND CSS variables AND JS values in your CSS-in-JS. Sometimes you just want a global CSS class; other times you need a value in your scoped CSS Modules. Thereâs no right or wrong here; simply do what saves you and your team the most time, both in writing new code and managing existing code.
- Reset component styles at the base level. This is a good practice beyond this blog post! You always want your componentâs styles to be isolated from one another, which means throwing some resets at the top level. When your components only look good in certain contexts but not others, youâre fighting a losing battle.
- Avoid global breakpoints if you can help it. Often people try and have a perfect breakpoint system in their app. It makes senseâitâs simple, and itâs encouraged in frameworks. But itâs bad because global breakpoints donât care about your actual content. They donât care that half of your components look weird as the page responds. Conversely, component-level media queries are always perfect for every component. Itâs a little extra code, sure, but itâs 100% scalable, and unlike global breakpoints, adjusting one componentâs breakpoint will never disrupt another. A little redundancy can save a lot of headache down the line. Give it a try and prove me wrong!
If this post was helpful, please give responsive-typography a star on GitHub (or give me feedback in the issues!)