A CSS-only, animated, wrapping underline.
Tags: css, tutorial
Published: 2020-07-20 18:32:56

---
title: A CSS-only, animated, wrapping underline.
published: true
tags: css,tutorial
canonical_url: https://nickymeuleman.netlify.app/blog/css-animated-wrapping-underline
---

Underlines are hard.
Complications quickly arise if you want to do anything fancier than the good ol' CSS [`text-decoration: underline`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration).
There are [a lot of different techniques](https://css-tricks.com/styling-underlines-web/). 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](https://www.cassie.codes/posts/creating-my-logo-animation/).

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 😢.

> Not only the links on [Cassie's website](https://www.cassie.codes/) are cool. The entire website is, from code to content.

## Goal

A <span style={{ color: "rgb(176, 251, 188)" }}>colored</span> underline beneath links that has a hover effect where the line retreats and is replaced by a <span style={{ color: "#feb2b2"}}>differently colored</span> 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`](https://developer.mozilla.org/en-US/docs/Web/CSS/background-image) CSS property.

A `background-image` can be a solid color by defining it as a [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient) that [transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/transition) from one color to the same color.


> Why am I using `background-image` and not `background-color` if I intend to use a solid color?

> Because many properties to manipulate the background only work if `background-image` is used.

> \- _insert CSS is hard meme here_ -


The size of the background is limited in height and takes up the full width of the anchor element by setting the [`background-size`](https://developer.mozilla.org/en-US/docs/Web/CSS/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`](https://developer.mozilla.org/de/docs/Web/CSS/background-repeat) to `no-repeat`.

The line is at the top of the anchor element! Positioning it with [`background-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/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.

{% codepen https://codepen.io/NMeuleman/pen/qBbQdpQ default-tab=css,result %}

## 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.

```css
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.

```css
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;
}
```

{% codepen https://codepen.io/NMeuleman/pen/vYLQNRg default-tab=js,result %}

### 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.

```css
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;
}
```
{% codepen https://codepen.io/NMeuleman/pen/NWxEGmL default-tab=css,result %}

## Three backgrounds

This almost satisfies the [goals](#goal). 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_](https://youtu.be/2AoxCkySv34) 🦇

I'll place this background on top of the other two by listing it first in the comma seperated value for [`background-image`](https://developer.mozilla.org/en-US/docs/Web/CSS/background-image).

> Don't forget!  
The first value for other `background-*` properties now also points to this newly added `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()`](https://developer.mozilla.org/en-US/docs/Web/CSS/calc) function is used to calculate both of these positions.

```css
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%;
}
```
{% codepen https://codepen.io/NMeuleman/pen/JjGeGar default-tab=css,result %}

## Tada 🎉

The anchor tag works and fits all the [goals](#goal)!

A big thank you to [Jhey "Jh3y" Tompkins](https://twitter.com/jh3yy)!

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!

{% twitter 1283155273113382914%}

I got to know Jhey in the [party corgi discord](https://discord.gg/partycorgi).
It's an awesome place that is filled with talented people, come hang out!