Flexible typography with CSS locks
In early 2012, I shared a formula for “molten leading”, or fluid line spacing. Experienced typographers know that long lines of text need more line spacing, but line spacing can be tighter for short lines of text. The problem is that on the web, our texts are flexible. So our line spacing needs to flex, too:
You can have perfect smooth scaling between any 2 font sizes over any viewport range. The font will start scaling and stop scaling exactly where you want.
That got me thinking. This formula is about more than line-height or font-size. This formula is a way of dynamically calculating any value between two extremes, relative to another set of extreme values — independent from media query breakpoints.
In canal and river navigation, a lock is a device used for raising and lowering vessels between stretches of water that are at different levels. That’s exactly what our formula accomplishes. Our formula is a CSS calc “lock”.
Take molten leading as an example. The gates are our paragraph’s minimum and maximum widths, and the water level is our line-height. Below the bottom gate, our line-height is a static 1.3. Above the top gate, our line-height is a static 1.5. The magic happens in between — as our paragraph’s width flexes, our line-height flexes dynamically to maintain an appropriate value. Here it is on CodePen:
And here’s what a CSS lock looks like in code:
line-height: calc(1.3em + (1.5 - 1.3) * ((100vw - 21em)/(35 - 21)));
To understand how the formula works within calc(), we’re going to work through it backwards.
- See the very last part? 35–21. That gives us the full range of our paragraph’s width. It resolves to 14, because 14em is the difference between our paragraph’s width at its most narrow and most wide.
- To the left of that, we’ve got 100vw–21em. Because of the way CSS calc works, this resolves to an em-based value — and gives us a numerator to place above the 14em we already figured out. So, for example, let’s say the viewport width (100vw) is equivalent to 34em. 34em–21em = 13em. Note that the viewport unit in this step is our secret sauce. The fact that this value can change dynamically with browser window width is what makes a dynamic line-height value possible.
- So the whole expression to the right of the multiplication sign gets distilled down to this: 13em / 14em, or 0.928571429em. Think of this as how close we are to the “upper gate” of our lock. If it’s near zero, we’re close to the lower gate. If it’s near one, we’re close to the upper gate.
- Moving to the left of the multiplication sign, we compute the difference between our maximum and minimum line heights. 1.5–1.3 = 0.2. This gives us the full range of our fluid line height.
- Now we multiply the full range of our fluid line height (step 4) by how far along we are toward the upper gate of our lock (step 3):
0.2 * 0.928571429em = 0.185714286em.
- Add this to the minimum line-height, and we end up with a dynamic, fluid line-height value: 1.3em + 0.185714286em = 1.485714286em. Hey look, it’s pretty close to our maximum line-height of 1.5 — about as close as our value from step 3 was to our maximum paragraph width.
- Enclosing all of this logic in a width-based media query lets us disable it by default (use our minimum line-height of 1.3), enable it when the viewport is wider than our lower gate (use a line-height lock above 21em) and disable it above our upper gate (use our maximum line-height of 1.5 above 35em). Because em-based media queries refer to the default font-size of 100%, I multiplied mine by the root font-size I chose for the typeface I’m using (1.15em). See the CSS code on CodePen.
Let me know if that didn’t make sense, or if there’s something I can clarify. One thing bothering me is that I couldn’t get this lock’s result to be unitless. I generally prefer to use unitless line-height values. I also wonder how the math might be different with container or element queries…
This concept of a CSS lock might be useful in many ways.
Consider Cyrus Highsmith’s idea of text block “tempo”. Not only line-height, but letter-spacing, word-spacing, and margins, could all flex relative to a text block’s width, loosening to slow the tempo, and tightening to speed it up.
Or, consider our future with variable fonts. Type’s x-height could flex relative to the font-size at which the type is set. Smaller type needs a taller x-height, while larger type looks better with a shorter x-height.
If these sound like nerdy details, that’s because they are. But so too, they are some of the reasons why the best printed typesetting often feels better than the same text on screens in a web browser. Typographers have traditionally paid close attention to these very nerdy details.
And CSS locks are useful for more than just typography. Use them wherever you need specific fluid behavior, contained within certain boundaries. “CSS locks” is a name I’m using for a specific kind of value calculation. I hope it’ll make communication easier and this concept more useful. But it begs the question: for what other formulae would it help to have names?
Comments are closed.
Real, real interesting, Tim. Can’t wait to try it out. It looks to me like I can finally do something I’ve wanted to do for the longest time – have the text “greek” below a certain threshold. Probably need a special font for it, but it looks like it can be done with these newfangled CSS properties.
This is the future…
Really cool idea. I’m excited to try it out.
“Think of this as how close we are to the “upper gate” of our lock. If it’s near zero, we’re close to the lower gate. If it’s near one, we’re close to the upper gate.”
What if you’re working with ranges that that are greater than 1? For example what if you wanted your line-height range to be from 1 to 3? I can’t seem to get that to work.
Oh, sorry. That works fine if the root is 1em. The issue I ran into has to do with changing the font-size of the document to a pixel value. I changed it to 10 so that I can easily experiment with the math. This messes things up though.
Hmm. Yeah. I would stick with relative lengths for sizing type in a flexible context: https://www.w3.org/TR/css3-values/#relative-lengths
See also, Davide’s comment below and my reply. I think part of what you were wrestling with here is related. Thanks for giving this all a try and sharing your experience, Mike.
Hi, I didn’t understand how will do this work?
Mohd, I did my best to explain how it works in this post. Is there a particular part that seems unclear? I’m happy to clarify or expand upon parts.
If you set upper line height to a much larger value (like 5em) the value seems to get much smaller than the lower value as it approaches the lower gate (but then snaps to the lower value below the lower gate.)
Does this mean the formula doesn’t really work for all values?
Davide! The problem here is that my media queries (21em and 35em) were based on the root font-size (100%), while my line-heights were based on the font-size I applied to the body element (1.15em). Multiply 21em and 35em each by 1.15 in the media queries, and everything works.
I’ve updated the example on CodePen: http://codepen.io/timbrown/pen/akXvRw?editors=0100
And I’ve updated this post as well. Nice catch! Thank you very much.
Ahhh … that was really nice … where were you a couple months ago when I hacked together the strangest version ever of this same concept 😉 … took me forever and yours is soo much cleaner … thank you stupendously for sharing!
Darn … sorry … I spoke too soon … I thought your pen was going to have all three … font size scale, leading and text block width on auto scale and lock. What I hacked together was a dynamic text block width that contained a font size that “locked” between 21px em equivalent at max text block width to 18px em equivalent at min text block width but that also had dynamic leading synced in as that font and text block sized up and down … heh … looks like my hack is going to have to stick around a bit longer 😉
It is a very useful tool, I can not wait to test it. Thank you for your guidance!
Thanks for this nice article and this great idea. I’m not a typo expert, but I believe that it will improve the reading experience substantially.
Based on your CodePen I build up a Sass version of it for my own (see http://codepen.io/andi1984/full/yJARkv/).
Hi Tim, thing you are writing about was one of my big concerns for last two months. I am a frontend developer. Note to make that am just at the beginning, but I made this: http://codepen.io/Ernedar/pen/ONYrJM based on some research this mixin can take what ever breakpoint top and bottom you give him and two numbers of font size for each of it and make fluid text on a web. In addition, second mixin can make ration fluid text for all of your headings. Most of my research is based on work of Hugo Giraudel. What do you think about this direction?
So I spent a few days working with this formula and my conclusion is that the explanation of how the formula works is wrong. It doesn’t work like that, because the variable part doesn’t resolve to a em value, but to a pixel value. This explains why people report mismatched values when this technique is applied to text that is not 16px.
So if we take the seven-step explanation of how the formula works, these are the parts that are wrong:
> 2. To the left of that, we’ve got 100vw–21em. Because of the way CSS calc works, this resolves to an em-based value — and gives us a numerator to place above the 14em we already figured out.
It actually resolves to a pixel value. One thing to understand is that “em” in a media query means “the base font-size set by the user and/or user agent”, which by default in most browsers is 16px, but it may vary on a few devices (like a TV browser) or if the user changed that value to something else (even to a preference that just says “bigger”).
When you do 100vw – 21em in a calc function, the browser will take the current viewport width in pixels, and substract 21 times the base font-size (in pixels). So this part is wrong too:
> So, for example, let’s say the viewport width (100vw) is equivalent to 34em. 34em–21em = 13em.
If we also say that the current base font-size is 16px, that means the current viewport width is 544px, and the browser is actually doing 544 – 21*16 = 208px.
So already at this point this fragment of the calculation is resolved to 208px, and not to 13em.
> 3. So the whole expression to the right of the multiplication sign gets distilled down to this: 13em / 14em, or 0.928571429em.
Correction: it gets distilled to: 208px / 14 = 14.857142857px.
> Think of this as how close we are to the “upper gate” of our lock. If it’s near zero, we’re close to the lower gate. If it’s near one, we’re close to the upper gate.
It we were closer to the lower gate it would indeed result in a value near to 0px. If we were close to the higher gate it would result in a value near the base font size (16px in our scenario, but possibly a different value).
> 5. Now we multiply the full range of our fluid line height (step 4) by how far along we are toward the upper gate of our lock (step 3):
0.2 * 0.928571429em = 0.185714286em.
Correction: 0.2 * 14.857142857px = 2.9714285714px.
> Add this to the minimum line-height, and we end up with a dynamic, fluid line-height value: 1.3em + 0.185714286em = 1.485714286em.
Correction: we end up with a calc(1.3em + 2.9714285714px), which can’t be resolved further down (except by the browser’s style system).
It’s a dynamic line-height, but only the 1.3em part is relative to the element’s font-size (note that we could express it as 1.3 or 130% with the same result). The part that resolves to pixels is actually relative to the UA’s base font-size.
If the element’s font-size is identical to the UA’s base font-size, this CSS lock will work perfectly. If it’s a bit different, there will be a visible jump. If it’s very different (for instance for a title that is twice or thrice the UA’s base font-size), then the result will be very wrong.
Florens, thanks for digging into this:
I know you were playing with this very early. Did you see Davide Rizzo’s comment, and my reply? I revised the media queries to eliminate such a visible jump: http://blog.typekit.com/2016/08/17/flexible-typography-with-css-locks/#comment-25760
Here’s a CodePen example with two locks — one for body text line-height, and another for heading font-size: http://codepen.io/timbrown/pen/QERPgY/
Okay, so correction to my correction, this part (from me) is wrong:
> When you do 100vw – 21em in a calc function, the browser will take the current viewport width in pixels, and substract 21 times the base font-size (in pixels).
After more testing, it turns out the 1em in a calc function equals 1em for the current property and the current element. So for line-height and most other properties, it’s 1 time the element’s computed font-size. For font-size, it’s 1 time the element’s parent’s computed font-size. This makes the behavior of (100vw – Nem) in a CSS lock a bit hard to understand and predict, so I’d avoid it. I’ve tried working with the rem unit instead (and not changing the root element’s font-size), and it works beautifully.
Tim, I hadn’t seen the version where you compensate the font-size by changing the breakpoints. It works but I find it a bit cumbersome, and would suggest keeping the original breakpoints (especially since you may have other styles using the same breakpoints in a real site or page), and using that correcting ratio inside the calc function instead:
Font-size example: http://codepen.io/fvsch/pen/xOoWKa
Line-height example: http://codepen.io/fvsch/pen/wWLXLK
And I think that we now have the bulletproof syntax for calc locks using em breakpoints.
Aha, I see. That’s a great point, and I like the idea of making sure that media queries are useful for more than just these calculations. However, and I might be wrong about this, I want my media queries to be based on the size of my body text (my “working” root font size) — not the standard 100%/16px to which they always refer.
I want to make layout decisions based on my typesetting. If text sized at 1.15em looks great at a maximum width of 33em plus 1em of buffer on either side (35em total, at 1.15 times the browser default), then I want to invoke my media query at exactly that point (40.25em at 100%).
I’ve published an article on this technique and similar CSS locks techniques, with an in-depth look of how they works (on the math side and CSS calc syntax): http://fvsch.com/code/css-locks/
Interesting, but did you try it on multiple platforms? I always have to make tons of PXs querys for MAC since it doesn’t seem to interpret EM and REM units well.
There are no specific problems with em and rem on Mac browsers. The font rendering engines involved are different, so the text may look a little bit different (often heavier and words tend to take a tiny little bit more width in my experience, though this doesn’t affect the font-size or line-height).
Hi Tim, thanks for your interesting code. I’ve always been interested in typography in responsive web design. Your technique gave me the following idea. I added scalable font-sizes in addition to your scalable line-heights:
font-size: calc(1em + (1.5 – 1) * ((100vw – 21em) / (35 – 21)));
The calculation is identical. 1em is the minimum font-size and 1.5em is the maximum font-size.
Now, you have to change a little bit of your code. For it to work you have to turn the first part of your calculation around: From the maximum line-height 1.5em you have to substract the right part of the code.
line-height: calc(1.5em – (1.5 – 1.3) * ((100vw – 21em)/(35 – 21)));
Then, line-height and font-size scale smoothly between the given breakpoints.
I hope this was helpful!
Holy Formula, Batman. It’s like I’m back in school in my calculus class. Seriously, though, this is really interesting and super useful. Can’t wait to try it out.
I love solutions like this – they look so simple and elegant because they’re short, but there’s really so much complexity to them when you really dig into it. As others have said, thank you for sharing!