The line-height property in CSS controls the space between lines of text. It is often set in a unitless value (e.g. line-height: 1.4;
) so that it is proportional to the font-size. It’s a vital property for typographic control. Too low and lines are awkwardly squished together; too high and lines are awkwardly far apart. Both inhibit readability. But you probably already know that.
In this article we’ll focus on some trickery. If you know (or can figure out) the exact value of line-height
, you can do some neat stuff!
Style each line of text a different color
There is no ::nth-line(), unfortunately. We can’t really even use <span>
s reliably, as there are tons of different things that can cause text to break at different points.
There is a way, albeit non-standard, to use the background of an element as the background of text.
.text {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
There is another trick where you can use a linear-gradient()
with color-stops such that the color doesn’t fade into another, it just abruptly ends and another starts. Let’s say we know that the line-height is 22px, we can make the gradient breaks right at that.
.text {
background-image: linear-gradient(
to bottom,
#9588DD,
#9588DD 22px,
#DD88C8 22px,
#DD88C8 44px,
#D3DD88 44px,
#D3DD88 66px,
#88B0DD 66px,
#88B0DD);
}
Combining those two tricks:
In a browser that doesn’t support the text background clipping, like Firefox, you would get solid bars of color behind the text. Maybe that’s cool and you like it. Maybe you’d rather just fall back to a solid color text. In that case, you can use @supports
to only apply it if supported anyway.
Also, since you’re using the value of line-height over and over, might be nice to variablize it. I’ll use SCSS here, but this would be kinda neat to do with real CSS variables someday so you could change it even after rendering and watch it all keep working.
$lh: 1.4em;
body {
font-size: 1em;
line-height: $lh;
}
@supports (-webkit-background-clip: text) {
p {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
to bottom,
#9588DD,
#9588DD $lh,
#DD88C8 $lh,
#DD88C8 $lh*2,
#D3DD88 $lh*2,
#D3DD88 $lh*3,
#88B0DD $lh*3,
#88B0DD);
}
}
Using this behavior at the top of the element is easiest. Here’s an example where the first few lines are altered for emphasis.
.text {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
to bottom,
rgba(white, 0.8),
rgba(white, 0.8) $lh,
rgba(white, 0.6) $lh,
rgba(white, 0.6) $lh*2,
rgba(white, 0.4) $lh*2,
rgba(white, 0.4) $lh*3,
rgba(white, 0.2) $lh*3,
rgba(white, 0.2));
}
It gets more difficult if we’re trying to target the last few lines of an arbitrary amount of text. In that case, we’ll need the first color band to go from the top to all-the-way-down-minus-a-few-lines. Fortunately we can do that with calc()
!
.text {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
to bottom,
rgba(white, 0.8),
rgba(white, 0.8) calc(100% - 66px),
rgba(white, 0.6) calc(100% - 66px),
rgba(white, 0.6) calc(100% - 44px),
rgba(white, 0.4) calc(100% - 44px),
rgba(white, 0.4) calc(100% - 22px),
rgba(white, 0.2) calc(100% - 22px),
rgba(white, 0.2));
background-position: bottom center;
}
There are other ways to do this kind of thing as well, like overlaying a pseudo element gradient (with pointer-events: none;
so it’s not annoying).
Lines Between Text
Using a similar technique to the solid-color-stops technique we used above, we can create a 1px line gradient that repeats exactly at the known line-height
. The easiest way is to use repeating-linear-gradient()
, as well as make sure all other elements play nicely (like padding that is also based on line-height).
.parent {
padding: $lh*2;
background: #082838;
background-image: repeating-linear-gradient(
to bottom,
rgba(white, 0) 0,
rgba(white, 0) $lh/1em*16px-1,
rgba(white, 0.1) $lh/1em*16px-1,
rgba(white, 0.1) $lh/1em*16px
);
}
In order to get the 1px line, we need to know what the line-height
is in pixels, then subtract one. The goal is that the gradient repeats at exactly the known line-height, so the last pixel in that space can be the line. Because we’ve left the body font-size at 1em, that’s 16px. And since the line-height
is set in ems, we can divide by 1em removing the unit, then multiply by 16px and subtract one when needed.
Position images one-per-line
Another thing you can do if you know the exact line-height
is to make background-size
match it, at least on the vertical axis. Then you can make it repeat vertically and it will line up one-image-per-line.
.text
background-image: url(image.svg);
background-size: $lh $lh;
background-repeat: repeat-y;
padding-left: $lh*2;
}
Demos
See the Pen One line of Text Dif Color by Chris Coyier (@chriscoyier) on CodePen.
I used a similar technique building style for a code block. Neat stuff.
On targeting the last few lines: Why mess with calc() instead of just changing the direction of the gradient to “to top” instead of “to bottom”?
On the lines example: This is really messy, without needing to be so. You can just use plain linear gradients with color stops at 1px, then use background-size in ems. NEVER EVER specify line heights in absolute lengths. It’s bound to break horribly when some careless developer changes the font size without bothering to change the line-height as well.
Yeah Charlotte caught me on that one too, WAY easier to just start the gradient at the bottom.
Thanks for the article Chris!
-> Could I get any work-around example for this issue. I know relation wit line-height & font-size. But, I need some clearance with first statement.
If anyone is wondering why the 1. example (multi-color) doesn’t work in Safari, but the 2. example (fade-to-gray) does, it’s because the CSS of 1. example relies on
@supports
(which Safari does not support).I love this emphasizing efect, I never thouht of it. I am definitely using it in some of my next projects :)
The gradient at the bottom is not just how to reverse the gradient, it’s solving a different problem, namely what it states in the article: “trying to target the last few lines of an arbitrary amount of text.”
Neat tricks, and they feel ‘good’ — as in, not abusing CSS (although it would be great to have an
::nth-line()
to make our intent more explicit). One thing I should point out, though, is that with that ‘lined paper’ effect, Blink has a nasty bug that makes the gradients render at the wrong scale. There are multiple bug reports about it, but no traction as far as I can see. If people could vote on a few of them, that might spur some action.Vox really <3s their gradients on all the things, but I noticed some of their pull quotes getting the treatment in a few stories recently:
http://charleston.eater.com/2015/1/27/7921157/after-28-years-in-business-crowds-still-line-up-for-hymans-seafood
Some of these fail in pretty weird ways if the zoom level is changed on the browser for any reason (which I actually know a lot of people do, as it’s easier to set the zoom on a browser than get their font sizes fixed correctly system-wide on high-res screens)
Neat and interesting effects. Fade out bottom effect is amazing, and I did not think that it was possible via CSS. Before that we had to use PNG half transparent images and absolute positions with Z-Index to create that effect over text box. Above in examples, it’s a very good practice to create that effect. Thanks for sharing.
These are really cool tricks, but browsing these on my Nexus 5 makes a couple of them fall apart, particularly in portrait.
Not knowing how the lines will wrap at various media queries can pretty dramatically change the impact. So don’t forget to test these on smaller devices.