Styling Underlines on the Web

Avatar of John D. Jameson
John D. Jameson on (Updated on )

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.

A thin, black underline with space around descenders
A thin, black underline with space around descenders—via Marcin Wichary, Crafting link underlines on Medium

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:

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

So what’s the best way to underline text?

It depends.

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.