Styling Underlines on the Web

Share this:

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.

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.

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.

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.

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.

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.

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:

An assortment of solid-color underline styles

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

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.

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

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 lowercase g (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.

👋

CSS-Tricks* is created, written by, and maintained by Chris Coyier and a team of swell people. It is built on WordPress and powered up by Jetpack. It is made possible through sponsorships from products and services we like.