Styling the underlines that sit beneath links can be a tricky business, and I constantly forget what’s the best approach depending on the situation. Thankfully however, John Jameson gets us up to speed in this guest post.
There are a bunch of different ways to style underlines. Maybe you remember the article Crafting link underlines on Medium. Medium wasn’t trying to do anything crazy; they just wanted to create a pretty normal-looking line below their text.
It’s a pretty basic underline, but it’s a good size and it skips descenders too. Definitely nicer than most browsers’ default. Well, it turns out Medium had to go through a lot of trouble to get that style on the web. Two years later, it’s still just as hard to style a good-looking underline.
Goals
What’s wrong with just using text-decoration: underline
? If we’re talking about the ideal scenario, an underline should be able to do the following:
- Position itself below the baseline
- Skip descenders
- Change color, thickness, and style
- Repeat across wrapped text
- Work on any background
I think these are all pretty reasonable things to ask for, but as far as I know, there’s no intuitive way to achieve all of them in CSS.
Approaches
So what are all the different ways we can underline text on the web?
Here are the ones I can think of:
text-decoration
border-bottom
box-shadow
background-image
- SVG filters
- Underline.js (canvas)
text-decoration-*
Let’s go down the list one by one and talk about the good and bad parts of each approach.
text-decoration
text-decoration
is the most straightforward way to underline text. You apply a single property and that’s all there is to it. At smaller sizes, it can look pretty decent, but increase the font size and the same line starts to feel clumsy.
See the demo.
The biggest problem with text-decoration
is its lack of customizability. It uses the color and font size of whatever text its applied to and there’s no cross-browser way to change the style. More on that later.
Good
- Easy to use
- Positioned below the baseline
- Skips descenders by default in Safari and iOS
- Wraps across lines
- Works on any background
Bad
- Can’t skip descenders in other browsers
- Can’t change color, thickness, or style
border-bottom
border-bottom
offers a good balance between being quick and customizable. This approach uses a tried-and-true CSS border, which means you can change color, thickness, and style with ease.
This is what border-bottom
looks like on inline
elements:
See the demo.
The big gotcha is how far away the underline is from the text — it’s completely below the descenders. You can address that by making elements inline-block
and reducing line-height
, but then you lose the ability to wrap text. Good for single lines, but not much else.
See the demo.
Additionally, you can use text-shadow
to cover up parts of the line near descenders, but you have to fake it by using the same color as whatever background it’s on. That means it works only for solid-color backgrounds and not gradients or images.
See the demo.
At this point, there are four properties styling a single underline. That’s a lot more work than text-decoration
.
Good
- Can skip descenders using
text-shadow
- Can change color, thickness, and style
- Can transition and animate color and thickness
- Wraps by default unless it’s an
inline-block
- Works on any background unless using
text-shadow
Bad
- Positioned far away and difficult to reposition
- A lot of unrelated properties to get it just right
- Janky text selection when using
text-shadow
box-shadow
box-shadow
draws an underline with two inset box shadows: one to create a rectangle and a second to cover it up. That means you’ll need a solid background for this to work.
See the Pen Underlines 5: box-shadow by John D. Jameson (@johndjameson) on CodePen.
You can use the same text-shadow
trick to fake gaps between the underline and the text’s descenders. But if the line is a different color from the text — or even just thin enough — it doesn’t really clash like text-decoration
does.
Good
- Can be positioned below the baseline
- Can skip descenders using
text-shadow
- Can change color and thickness
- Wraps across lines
Bad
- Can’t change style
- Doesn’t work on any background
background-image
background-image
comes the closest to everything we want and with the fewest gotchas. The idea is that you use linear-gradient
and background-position
to create an image that repeats itself horizontally across lines of text.
You’ll have to display: inline;
this approach too.
See the demo.
This approach doesn’t have to use linear-gradient
either. You can bring your own background image for some cool effects.
See the demo.
Good
- Can be positioned below the baseline
- Can skip descenders using
text-shadow
- Can change color, thickness (allows half pixels), and style
- Works with custom images
- Wraps across lines
- Works on any background unless using
text-shadow
Bad
- The image can resize differently across resolutions, browsers, and zoom levels
SVG filters
Here’s an approach I’ve been toying around with: SVG filters. You can create an inline SVG filter
element that draws a line and then expands the text to mask out parts of the line we want to be transparent. Then you can give the filter
an an id
and reference it in CSS with something like filter: url(‘#svg-underline’)
.
The advantage here is that the filter adds transparency without relying on text-shadow
. That means you can skip descenders on top of any background, including gradients and background images! This one works only on a single line of text though, so heads-up on that.
See the demo.
Here’s what it looks like in Chrome and Firefox:
Browser support in IE, Edge, and Safari is problematic. It’s hard to test for SVG filter support in CSS. You can use @supports
with filter
, but that only tests if the reference works — not the applied filter itself. My approach ends up doing some pretty gross browser sniffing, so double heads-up on that too.
Pros
- Positioned below the baseline
- Skips descenders
- Able to change color, thickness, and style
- Works on any background
Cons
- Doesn’t wrap across lines
- Doesn’t work in IE, Edge, or Safari, but you can fall back to
text-decoration
. Safari’s underlines look good anyway.
Underline.js (Canvas)
Underline.js is fascinating. I think it’s super impressive what Wenting Zhang was able to do with JavaScript and some attention to detail. If you haven’t seen the Underline.js tech demo before, definitely stop reading for a minute and check it out. There’s a fascinating nine-minute-long talk on how it works, but I’ll give you the short version: it draws underlines with <canvas>
elements. It’s a novel approach that works surprisingly well.
Despite the catchy name, Underline.js is a tech demo only. That means you won’t be able to drop it into any projects without modifying it a whole bunch first.
It’s worth bringing it up here as a proof of concept. <canvas>
has the potential to create beautiful, interactive underlines, but you’ll have to write some custom JavaScript to get them working.
text-decoration-* properties
Remember the “more on that later” part? Well, here we are.
text-decoration
works fine by itself, but you can add a few experimental properties to customize the way it looks:
text-decoration-color
text-decoration-skip
text-decoration-style
Just don’t get too excited. You know, browser support.
text-decoration-color
text-decoration-color
lets you change an underline’s color separately from its text color. The property even has better-than-expected browser support — it works in Firefox and prefixed in Safari. Here’s the catch: If you’re not clearing descenders, Safari puts the line on top of the text. 🙃
Firefox:
Safari:
text-decoration-skip
text-decoration-skip
toggles skipping descenders in underlined text.
This property is non-standard and works only in Safari right now, so you need the -webkit-
prefix to use it. Safari enables this property by default though, which is why underlines skip descenders even on websites that don’t specify it.
If you’re using Normalize, know that recent versions disable the property to keep things consistent between browsers. You need to flip it back on if you want those dreamy underlines.
text-decoration-style
text-decoration-style
offers the same sorts of lines you’d expect from border-style
, but adds in wavy
lines too.
Here are the different values you can use:
dashed
dotted
double
solid
wavy
Right now, text-decoration-style
works only in Firefox, so here’s a screenshot:
Look familiar?
What’s missing?
The text-decoration-*
properties are far more intuitive than using other CSS properties to style underlines. But if we take another look at our earlier requirements, these properties don’t offer a way to specify line thickness or position.
After doing a little research, I came across these two properties:
text-underline-width
text-underline-position
It looks like they were pitched in earlier drafts of CSS, but never implemented due to lack of interest. Hey, don’t blame me.
Takeaways
So what’s the best way to underline text?
For small text, I recommend using text-decoration
and then optimistically applying text-decoration-skip
on top. It looks a little bland in most browsers, but underlines have looked that way forever and people don’t seem to mind. Plus there’s always the chance that if you’re patient enough, all your underlines will look awesome later on without you having to change a thing.
For body text, probably use the background-image
approach. It works, it looks great, and there are Sass mixins for it. You can probably omit text-shadow
it if the underline is thin or in a different color from the text.
For single lines of text, use border-bottom
and whatever other properties you want to go with it.
And for skipping descenders on top of a gradient or background image, try using SVG filters. Or just avoid using an underline altogether.
In the future when browser support is better, the answer is text-decoration-*
all the way.
Also see Benjamin Woodruff’s post CSS Underlines Suck, which coincidentally treads this ground in a similar way.
There is also psuedo elements with
position: absolute
,bottom: <value>
andheight: <value>
. I usually use this for button hover with underline animation:Ah, good call! I forgot all about pseudo-elements. They have a lot of flexibility — especially for transitions and animations — but suffer from the same wrapping issue as a lot of the other approaches. If you keep the constraints in mind, they can still be a lot of fun. Great example :D
Great roundup! I’ve got one more example for you in this demo: http://codepen.io/tomhodgins/pen/PGoVGJ
Look for the ‘Underline’ example in that pen. It creates two overlapping ovals and allows you to create a curved underline as wide as the word. The only catch is you need to have a solid background colour (and use that BG color for the oval on top) for the effect to work
One thing that seems nice about the native
text-decoration-skip
is that it actually skips the entire descender— this matters when you have things like a lowercaseg
(all the other options leave a small bit of the underline in the bowl, which I think looks super awkward).I agree, although I like the way that the other techniques leave the ends of the underline shaped to follow the descender.
text-decoration-skip
looks as though it’s just following the bounding box of the glyph.What would be nicer would be to expand the outline of the shape, but to use a non-zero winding rule rather than the more common even-odd to distinguish the inside and the outside of the glyph. I’m not sure that’s possible for any of the techniques here.
Actually that wouldn’t really work either. It would help with the lovely closed bowl of the g in the examples, but would still not work with y or recursive j shapes.
Nice round up.
Check out this background linear gradient method here:
http://stackoverflow.com/a/32145544/947370
All of your demo links appear to be broken. They all lead to the “You’ve ripped a hole in the fabric of the internet” error page.
Using NoScript? Allow codepen.io, cloudfare.com, wp.com and, {sob} google-analytics.com
Please be aware that high contrast mode may remove backgrounds and shadows, and might cause links to be unperceived. I think it’s a good think to point out.
I ended up with a JS test (plugged in Modernizr via its
addTest
method) and thems-high-contrast media
query to fall back to classic underlines when high contrast mode is detected.Here’s a pen styling underlines with gradients: http://codepen.io/ffoodd/pen/jbRMqJ/
And a gist for the high contrast detection plugged in Modernizr: https://gist.github.com/ffoodd/78f99204b5806e183574
And what about the mix-blend-mode to get rid of the background ? http://codepen.io/Kingeek/pen/PGZdyz
nice one!
Great article!
Wish i will remember it when i need it. =D
Why do people want to skip descenders? It looks much more distracting to me, and not what I would expect an underline to look like.
Nice article, but I think border bottom with text-shadow looks great.
+1 for pseudo elements. You can also do things like set the width relative to the parent also. Works well for headlines.
Since publication of this, there has been more tools added to the CSS toolbox for underlines, for example:
text-decoration-thickness