Marcin Wichary from Medium recently wrote a similar post on this topic, which I highly recommend. This post summarizes an expansion on his work, particularly as it relates to this comment thread on text-shadow.
We read news articles, blog posts, tweets, emails, text messages, books, restaurant menus, TV Guides, subtitles, road signs, product labels, snail mail, code, and more. And that’s not counting the hundreds of user interfaces we computer-savvy folk interact with every day.
The web wasn’t originally built as a bastion of typographical excellence. Until quite recently, the best web type didn’t even come close to the best book. But to the credit of standards bodies, browser vendors, CSS experts, and others, that gap is narrowing.
I’m hoping the trick I’m about to show you gets us just a little bit closer.
In CSS, links are styled with text-decoration: underline by default. This gives them the following look:
Over time, this historical default has led to the underline being a fairly universal symbol for link.
Since each OS+browser implements this underline itself, its weight and placement can vary quite a bit, leading to inconsistencies within designs across systems.
Further, very few implementations take into account what are known in typography as descenders, or glyphs which extend below the text baseline. These include letters like g, j, p, q, y, and in some typefaces, numerals like 3, 4, 5, 7, and 9.
iOS 8 recently started handling descenders better. Here’s a screenshot from the Messages app.
Here’s another from Safari:
Apple has historically been a leader in digital typography. Ever since Steve Jobs fatefully listened in on a calligraphy class, typography has been a central design element at Apple.
This implementation is an improvement, but personally I find the underline extends too far to the left of the “h” and too far to the right of the final “g”. So we’re going to try to do even better.
So we built an app for Eager, called SmartUnderline, which does just that.
A smarter underline
All of these concerns led me to the conclusion that I draw this line myself. When I began writing The Magic of CSS, getting the typography right was essential. I’d read Medium’s post and decided to give this text-shadow thing a shot. (If you’re curious, here’s my first implementation, back in March.)
@import nib selectionColor = #b4d5fe textShadowToCropUnderline(color) text-shadow .03em 0 color, -.03em 0 color, 0 .03em color, 0 -.03em color, .06em 0 color, -.06em 0 color, .09em 0 color, -.09em 0 color, .12em 0 color, -.12em 0 color, .15em 0 color, -.15em 0 color linkUnderlines(backgroundColor, color) color color text-decoration none textShadowToCropUnderline backgroundColor background-image linear-gradient(backgroundColor, backgroundColor), linear-gradient(backgroundColor, backgroundColor), linear-gradient(color, color) background-size .05em 1px, .05em 1px, 1px 1px background-repeat no-repeat, no-repeat, repeat-x background-position 0% 90%, 100% 90%, 0% 90% &::selection textShadowToCropUnderline selectionColor background selectionColor &::selection textShadowToCropUnderline selectionColor background selectionColor &:before, &:after, *, *:before, *:after text-shadow none &:visited color color
There’s a lot going on here, so I’ll break it down in a second. But first let me just show you how simple this mixin is to use once you import it into your project.
a linkUnderlines #fff, #000
That’s it. Just call linkUnderlines with a background color and text color. As an example, here’s how we use the mixin in the Eager blog:
blogTextColor = #333332 blogBackgroundColor = #fff .blog-post color blogTextColor a:not(.without-underline) linkUnderlines blogBackgroundColor, blogTextColor
The :not(.without-underline) part is not necessary, but I recommend using it to give yourself a way to opt-out a specific link.
<a class="without-underlines">I’m a special link! 🙂</a>
Alright, you’ve waited patiently, so here’s a breakdown of what all of this stuff does.
First we define a helper mixin textShadowToCropUnderline which draws 12 text-shadows, half to the left, half to the right, spaced every .03em. This is called within linkUnderlines with the background color you pass, since its job is to draw the letters below the text but above the background image. It is also called within the ::selection (and associated -moz- prefix for Firefox) block to set a different background color for cropping when text is selected.
textShadowToCropUnderline(color) text-shadow .03em 0 color, -.03em 0 color, 0 .03em color, 0 -.03em color, .06em 0 color, -.06em 0 color, .09em 0 color, -.09em 0 color, .12em 0 color, -.12em 0 color, .15em 0 color, -.15em 0 color
This is the meat of the trick, and took a fair amount of fine-tuning. A little detail:
- The choice to use ems comes from from general CSS best-practices. The em unit provides greater design flexibility and portability of styles across projects.
- The farthest shadow extending .15em comes from fine-tuning, but the idea is that this is roughly the width of the vertical bar in an “I” or “T”, or the amount of breathing room you want around each descender. Also, if you make this too small you’ll see little dots inside of the loop of your lowercased “g”.
- The choice of spacing by .03em comes from balancing two things: performance and flexibility. The more text shadows you draw, the more you’re asking the computer to do, and the slower this can become on mobile or older browsers. The fewer you use the less resolution you have and the more likely you may end up seeing a dotted underline near your descender instead of a solid cut-out..03 × 16 = .48, so .03em is less than half a pixel in the browser default font-size of 16px. So even for font sizes as small as 8px we should always be drawing a continuous block of shadows.
- The choice to use no spread in the text-shadow comes from a desire to make the math in the previous point simpler, and again for performance.
- When a user selects this text, the selection color is used for the text-shadow color instead of the background color.
Now let’s take a look at all of the background-image tricks.
background-image linear-gradient(backgroundColor, backgroundColor), linear-gradient(backgroundColor, backgroundColor), linear-gradient(color, color) background-size .05em 1px, .05em 1px, 1px 1px background-repeat no-repeat, no-repeat, repeat-x background-position 0% 90%, 100% 90%, 0% 90%
Note: I’ve written the background as multiple lines for readability, however Stylus syntax requires this to be written on one line.
As I mentioned above, I wasn’t satisfied with how far the line extends to the sides in other implementations (even including Medium and iOS). Since the text-shadow trick already necessitates a solid background color, it’s no additional cost to have to “white-out” part of the line as well. That’s what this does.
Basically we draw three lines: one which is the actual underline, drawn in the text color, and two which are used to cover up the tips of that line on each end, drawn in the background color.
A few more details:
- The .05em is therefore the amount the line gets chopped off on each end.
- The 90% is the vertical placement of the line. This may require minor adjustments based on the typeface and font-size, but 90% works pretty well in most cases.
- Finally, we use 1px here (instead of ems) to guarantee that in every non-retina environment, we indeed get only a hairline. If you desire a thicker line, or want this to grow with your text size, change it to something like .0625em. If you want a line with an even thinner look, call Stylus’s ligthen (or darken when the background color is dark) on the last linear-gradient, like this:
background-image linear-gradient(backgroundColor, backgroundColor), linear-gradient(backgroundColor, backgroundColor), linear-gradient(lighten(color, 20%), lighten(color, 20%))
While this trick works great in blog posts and other situations in which you have a lot of control over the look of your text, there are a couple things you should note:
- It requires that the text be on top of a solid background color.
- It makes an assumption about selection color, which is OS- and browser-dependent.
- On mobile and older browsers, it may have minor performance penalties. In our testing, we didn’t find much if any impact, especially since the text-shadows don’t use any spread value. But since you’re drawing new things that didn’t used to be there, it’s worth noting.
That being said, it works great on blogs. At Eager, we use it throughout our app, for example on links in our in-app messaging.
Smart underlines for every website
We couldn’t just stop there. After building this, I thought, wouldn’t it be cool if anybody could just add a script to their page and have all of this complicated underline stuff happen automatically?
So we built an app for Eager, called SmartUnderline, which does just that.
A note on the underline
If you’re new to typography, I can’t recommend enough Butterick’s Practical Typography. It covers just about everything you’d likely encounter when designing and building a web interface, and his advice is spot-on. With perhaps one exception.
His chapter on underlining essentially says don’t.
“In a printed document, don’t underline. Ever.
It’s ugly and it makes text harder to read.”
And in printed typography, he’s right. But when it comes to the web, I believe the universality of underlines representing links trumps this rule,especially if you can do it right. 😉