• Date

  • By

    • Nicky Meuleman
  • Tagged

    • Howto
    • CSS
  • Older post

    Syntax highlighting codeblocks with theme-ui

  • Newer post

    Love errors

Table of contents
  1. Goal
  2. Use the background
  3. Two backgrounds
    1. Transitioning the
  4. Three backgrounds
    1. Transitioning the
  5. Tada 🎉

A CSS-only, animated, wrapping underline.

A CSS-only, animated, wrapping underline.

Underlines are hard. Complications quickly arise if you want to do anything fancier than the good ol’ CSS text-decoration: underline. There are a lot of different techniques. Unfortunately, they nearly always come with significant drawbacks.

I ran into some of these drawbacks when I wanted to “borrow” the styling from the links in a Cassie Evans blogpost.

The links there have this awesome effect when you hover over them: The underline retreats and gets replaced by a new one, leaving a bit of space between the two while the transition happens.

The issue I ran into: Links on my blog often wrap to a different line and that means part of the link would not be underlined 😢.


A colored underline beneath links that has a hover effect where the line retreats and is replaced by a differently colored line. The lines should not touch during this animation, leaving some space between them.

Links that wrap onto new lines should have the underline beneath all lines.

Use the background

There are many different ways to underline a piece of text. The method I ended up using that met all of the requirements was: Using the background-image CSS property.

A background-image can be a solid color by defining it as a linear-gradient that transitions from one color to the same color.

The size of the background is limited in height and takes up the full width of the anchor element by setting the background-size to 2px and 100% respectively.

This still ends up covering the entire background, because now it repeats over and over until it covers the entire background. So I stopped it from being naughty by setting background-repeat to no-repeat.

The line is at the top of the anchor element! Positioning it with background-position set to 0 100% places it at the left edge, and 100% from the top edge of the anchor element.
In other words, at the bottom… It’s at the bottom now.

Two backgrounds

To use and manipulate multiple background images, set multiple values for the background-* properties, seperated by a comma.

The first entry in a comma seperated list is on top, with each following entry a layer behind it.

The background of the following anchor element will be entirely black (#000000). The white (#FFFFFF) background is there, but it’s not visible because it’s covered by the black one.

a {
background-image: linear-gradient(#000000, #000000), linear-gradient(#ffffff, #ffffff);

In the example below, two backgrounds are set. Both at the bottom, making one overlap the other.

a {
color: #dfe5f3;
text-decoration: none;
background-image: linear-gradient(rgb(176, 251, 188), rgb(176, 251, 188)),
linear-gradient(#feb2b2, #feb2b2);
background-size: 100% 2px, 100% 2px;
background-position: 100% 100%, 0 100%;
background-repeat: no-repeat, no-repeat;

Transitioning the background-size

Notice how the background-position is different, while it makes no visible difference? One is anchored to the left side, the other is anchored to the right side.

Next, I’ll be transitioning between one background taking up the full width normally and no width on hover while the second background does the opposite.

That anchoring will affect which point each background moves from/towards.

a {
color: #dfe5f3;
text-decoration: none;
background-image: linear-gradient(rgb(176, 251, 188), rgb(176, 251, 188)),
linear-gradient(#feb2b2, #feb2b2);
background-size: 100% 2px, 0 2px;
background-position: 100% 100%, 0 100%;
background-repeat: no-repeat;
transition: background-size 2s linear;
a:hover {
background-size: 0 2px, 100% 2px;

Three backgrounds

This almost satisfies the goals. The only thing missing is the space between the two lines.

That space can be faked by moving a block with the same color as the background. What is that block? You guessed it: another background.

What is better than 2 background? Three backgrounds!

Three backgrounds .. ah ah ah 🦇

I’ll place this background on top of the other two by listing it first in the comma seperated value for background-image.

The width and height are set by background-size. While the height is set to the same size as the other backgrounds (2px in this example). This time, the width is set to be a fairly small 20px.

Transitioning the background-position

To make the background-colored block invisible before hovering over the anchor element, the background is given a negative background-position that places it to the left of the element, and thus, completely off the screen.

After hovering on the anchor, the block should move to the opposite side of the underline until it is completely offscreen again.

The calc() function is used to calculate both of these positions.

a {
color: #dfe5f3;
text-decoration: none;
background-image: linear-gradient(#222b40, #222b40), linear-gradient(
rgb(176, 251, 188),
rgb(176, 251, 188)
), linear-gradient(#feb2b2, #feb2b2);
background-size: 20px 2px, 100% 2px, 0 2px;
background-position: calc(20px * -1) 100%, 100% 100%, 0 100%;
background-repeat: no-repeat;
transition: background-size 2s linear, background-position 2s linear;
a:hover {
background-size: 20px 2px, 0 2px, 100% 2px;
background-position: calc(100% + 20px) 100%, 100% 100%, 0 100%;

Tada 🎉

A big thank you to Jhey “Jh3y” Tompkins!

He is a magician with all things CSS/animation and I’m really glad I reached out to him.

I asked him a question when I was trying to figure this out. He not only answered it and taught me about the background-position technique mentioned above. He took it as a fun challenge and made an awesome proof of concept!

Designed and developed by Nicky Meuleman

Built with Gatsby. Hosted on Netlify.