---
title: "CSS Hover Underline Animations That Work With Wrapping"
description: "Animate link underlines with ::after and scaleX, a background-size gradient that survives line wraps, or text-decoration transitions, accessibly."
url: https://frontendhelpers.com/css-hover-underline-animation
updated: 2026-10-12
---

# CSS hover underline animations that survive line wraps

Three ways to animate a link underline, what each one does when the link breaks across two lines, and how to keep the effect usable with a keyboard, on a touchscreen and with reduced motion.

CSS recipe · By [AurumCodeWorks](https://frontendhelpers.com/about) · 7 min read · Updated 12 October 2026

[Left](#) [Centre](#) [Through](#)

**Scaled ::after** — three `transform-origin` settings; hover or Tab to each link.

::after[A link long enough to wrap](#) here.

Gradient, slice[A link long enough to wrap](#) here.

Gradient, clone[A link long enough to wrap](#) here.

**Wrapped links** — the same two-line link with each technique; only the gradients underline both lines.

Links in running text keep their underline. On hover or focus, [this underline thickens and moves closer](#) instead of appearing from nowhere.

**Real underline** — thickness, offset and colour transitions on a `text-decoration` that is always visible.

## Three ways to draw an underline you can animate

A plain underline is either on or off. `text-decoration-line` animates discretely, so switching it from `none` to `underline` happens in one step however long the transition is. To make a line grow, draw or thicken, you need something with values in between, and CSS offers three: a pseudo-element you can scale, a background gradient whose size you can change, and the newer text-decoration longhands that take lengths and colours.

They look identical on a one-word link. They differ when a link breaks across two lines, which is exactly the case most underline snippets never test. The scaled pseudo-element is the version in our [Underline Slide card](https://frontendhelpers.com/css-hover-effects#anim-hover-underline), and a tab-bar variant lives in the [Tab Underline Slide card](https://frontendhelpers.com/css-ui-feedback-animations#anim-tab-underline). The demos above work with a mouse or the Tab key. For more ready-made snippets, browse the [CSS hover effects](https://frontendhelpers.com/css-hover-effects) category.

## The pseudo-element underline: scaleX and transform-origin

The link gets `position: relative`, and an empty `::after` is absolutely positioned along its bottom edge, 2px tall and filled with `currentColor`. At rest it is squashed to nothing with `scaleX(0)`; on hover or keyboard focus it scales back to full width.

```css
.ul-pseudo {
  position: relative;
  color: #e2e8f0;
  text-decoration: none;
}
.ul-pseudo::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.ul-pseudo:focus-visible::after { transform: scaleX(1); }
@media (hover: hover) {
  .ul-pseudo:hover::after { transform: scaleX(1); }
}
```

Only `transform` changes, so the browser paints the bar once and scales it on the compositor, which makes this the cheapest of the three techniques to run. `currentColor` keeps the bar in step with the text, even if the link colour also changes on hover. Setting `left` and `right` to 0 stretches the bar between the link’s edges without a width calculation, and the negative `bottom` drops it clear of descenders.

`transform-origin` decides where the growth starts. `left` grows rightwards. `center` expands outwards from the middle, the look used by the tab underline in our library. The third variant draws in from the left and leaves to the right:

```css
.ul-centre::after { transform-origin: center; }
.ul-through::after { transform-origin: right; }
.ul-through:focus-visible::after { transform-origin: left; }
@media (hover: hover) {
  .ul-through:hover::after { transform-origin: left; }
}
```

The trick is that the origin changes instantly while the scale transitions. On hover the origin jumps to the left while the bar is still zero width, so the jump is invisible, and the bar grows rightwards. On leave the origin snaps back to the right and the bar shrinks towards it, so it seems to slide out of the far side. Keep `transform-origin` out of the transition list, or the bar slides sideways mid-animation.

## Why the pseudo-element breaks on wrapped links

An absolutely positioned child of an inline element uses that inline as its containing block. When the link wraps, the browser splits the inline into a fragment on each line, but `::after` is still a single box. CSS defines the containing block as running from the start of the first fragment to the end of the last, a shape that matches neither line. Hover the top example in the second demo: the bar is drawn once, never under both lines. In current Chrome it sits under the last line only; other engines can place it differently, and the result also shifts with where the line happens to break.

In navigation that is rarely a problem. Menu items are short, and `white-space: nowrap` or `display: inline-block` keeps each one on a single line. In running text, card headings and any link that a translation might lengthen, you cannot promise a single line, so the pseudo-element is the wrong tool.

## A gradient underline that follows the text

Backgrounds on inline elements are painted fragment by fragment, so a background can follow text round a wrap where a pseudo-element cannot. This technique paints a single-colour “gradient” (`linear-gradient(currentColor, currentColor)` is simply a way to turn one colour into a background image), sizes it to a thin strip along the bottom of the link, and animates `background-size` from 0% to 100% to draw the line.

```css
.ul-grad {
  color: #e2e8f0;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 2px;
  transition: background-size 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.ul-grad:focus-visible { background-size: 100% 2px; }
@media (hover: hover) {
  .ul-grad:hover { background-size: 100% 2px; }
}
```

`background-position: 0 100%` pins the strip to the left and the bottom, and `no-repeat` stops it tiling up the link. The second value of `background-size` is the line’s thickness, so 2px stays 2px whatever the width.

How the strip crosses the wrap depends on `box-decoration-break`. With the default, `slice`, the browser treats the fragments as one long line laid end to end, sizes the background against that total width and then cuts it into pieces. So 100% means the whole link, and the line draws along the first line before continuing on the second, in reading order. With `clone`, each fragment gets its own copy of the background, sized against that fragment alone, so every line draws at the same time:

```css
.ul-clone {
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
```

Support needs a little care. MDN’s [box-decoration-break reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/box-decoration-break) marks the property as limited availability: Firefox has supported it unprefixed since version 32 and Chrome and Edge since version 130, while Safari only recognises `-webkit-box-decoration-break`, and the prefixed property applies only to inline elements. Links are inline, so writing both declarations covers current browsers, and anywhere `clone` is ignored you simply get the `slice` behaviour, which still works.

The in-left, out-right idea carries over too. Set `background-position: 100% 100%` at rest and `0 100%` on hover, and transition only `background-size`; the position jumps while the strip has no width, just as the transform origin did.

The price is paint rather than compositing: changing `background-size` repaints the link on every frame, which is trivial for a few words but not for hundreds of elements at once. Our guide to [CSS animation performance](https://frontendhelpers.com/css-animation-performance) explains the difference.

## Animating the real underline

The simplest modern option keeps a genuine text decoration and animates its details. Three longhands take values the browser can interpolate. MDN’s [text-underline-offset reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-underline-offset) lists it as animatable by computed value type and widely available since November 2020; `text-decoration-thickness` animates the same way and has been widely available since March 2021, and `text-decoration-color` animates as a colour.

```css
.ul-deco {
  color: #67e8f9;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
  text-decoration-color: rgba(103, 232, 249, 0.45);
  transition:
    text-decoration-thickness 0.2s ease,
    text-underline-offset 0.2s ease,
    text-decoration-color 0.2s ease;
}
.ul-deco:focus-visible {
  text-decoration-thickness: 3px;
  text-underline-offset: 0.15em;
  text-decoration-color: #67e8f9;
}
@media (hover: hover) {
  .ul-deco:hover {
    text-decoration-thickness: 3px;
    text-underline-offset: 0.15em;
    text-decoration-color: #67e8f9;
  }
}
```

Because this is a real underline, it wraps with the text, breaks around descenders where the browser skips ink, and needs no positioning or extra layers.

Two caveats. First, interpolation needs the same kind of value at both ends. `auto` and `from-font` are keywords, so a transition from `text-decoration-thickness: auto` to `3px` jumps rather than animates; use lengths throughout, and note that `text-underline-offset` in `em` scales with the font. Second, underlines are drawn on the pixel grid, so going from 1px to 3px has only a couple of visible steps. Keep the transition short and let the offset and colour carry most of the change, as the third demo does: the line moving 0.15em closer to the text while it brightens reads as smooth even though the thickness steps.

| Technique | Survives line wraps | Animatable properties | Effort |
| --- | --- | --- | --- |
| `::after` + `scaleX()` | No: one bar for a link split over several lines | `transform` (and `opacity`); origin switched instantly | Medium: positioned link, pseudo-element, two states |
| `background-image` gradient + `background-size` | Yes: in reading order with `slice`, all lines at once with `clone` | `background-size`, `background-position` | Low: one element, a few declarations |
| `text-decoration-*` longhands | Yes: it is the browser’s own underline | `text-decoration-thickness`, `text-underline-offset`, `text-decoration-color` (lengths and colours, not keywords) | Lowest: no extra boxes or positioning |
| Toggling `text-decoration-line` | Yes | None: discrete, so the line just appears | None, but nothing to animate |

## Keep the underline on links in prose

Most hover-underline snippets begin with `text-decoration: none`, which leaves the links in a paragraph identified by colour alone until someone points at them. The W3C’s [Understanding document for 1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) lists links that are not visually evident without colour vision as a failure (F73). Its sufficient technique for colour-only links, G183, requires a contrast ratio of at least 3:1 between the link colour and the surrounding text colour, on top of the 4.5:1 that both need against the background. On a dark page with light grey body text, finding a link colour that meets all three ratios is hard, and a hover cue never reaches people on touchscreens.

So split the job. Links inside paragraphs, lists and table cells keep a visible underline at rest and use the third technique to emphasise it on hover and focus. The draw-on underlines from the first two techniques belong where position already says “this is a link”: navigation bars, footers, tab lists and card headings. Even there, mark the current page with more than a colour change. Our [accessible website guide](https://frontendhelpers.com/accessible-website-guide) covers the rest of that checklist.

## Focus, touch screens and reduced motion

**Keyboard focus.** Every rule that responds to `:hover` also responds to `:focus-visible`. In the demos the focus rules sit outside any media query so they always apply, and the browser’s focus outline stays, because a 2px line is a weak focus indicator on its own. `:focus-visible` rather than `:focus` means clicking a link with a mouse does not leave its underline drawn until focus moves elsewhere.

**Touch screens.** Phones and tablets cannot hover, but their browsers apply `:hover` on tap so that hover-only menus still open, which leaves the effect stuck on afterwards. MDN’s [hover media feature reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/hover) explains that `(hover: hover)` matches when the primary input mechanism can conveniently hover, and `(hover: none)` when it cannot or can only emulate it with a long press. Wrapping the hover rules in `@media (hover: hover)` removes the sticky state, and since the animation is decoration, nothing is lost. It is another reason prose links need their underline at rest: on a touchscreen, the rest state is all anyone sees.

**Reduced motion.** A line growing across a word is small movement, but people who turn on their operating system’s reduce-motion setting have asked for less of it, and the information is the change of state, not the motion. Remove the transitions so the underline appears in its final form at once:

```css
@media (prefers-reduced-motion: reduce) {
  .ul-pseudo::after,
  .ul-grad,
  .ul-deco { transition: none; }
}
```

Only the transitions go; the hover and focus states still apply, instantly.

## Frequently asked questions

### Why does my animated underline break when a link wraps onto two lines?

An ::after bar is a single box positioned against a link that the browser has split into one fragment per line, so it cannot underline both lines. Paint the line with a background-image gradient and animate background-size instead, or animate text-decoration-thickness and text-underline-offset. Both follow the text across the wrap.

### Can you animate text-decoration in CSS?

Not on and off: text-decoration-line is discrete, so an underline appears or disappears in one step. Its details are animatable, though. text-decoration-thickness, text-underline-offset and text-decoration-color all transition smoothly, provided both ends use lengths or colours rather than keywords such as auto or from-font.

### Is it OK to remove the underline from links?

In navigation, footers and tab bars, where position already says the item is a link, yes. In body text it leaves colour as the only cue, which fails WCAG 1.4.1 Use of Color unless the link colour also has 3:1 contrast with the surrounding text. Keep the underline in prose and animate its thickness, offset or colour instead.

### How do I stop hover underlines sticking on phones?

Touch browsers apply :hover after a tap and leave it there until the user taps elsewhere. Put the :hover rules inside @media (hover: hover), which only matches when the primary input can hover, and keep the matching :focus-visible rules outside the query so keyboard users always get the effect.

## Keep going

- [CSS hover effects](https://frontendhelpers.com/css-hover-effects): Underline slides, lifts, tilts and gradient shifts, each with markup and CSS to copy.
 
- [CSS UI feedback animations](https://frontendhelpers.com/css-ui-feedback-animations): Tab underlines, progress fills and other small signals that confirm what the user just did.
 
- [Accessible website guide](https://frontendhelpers.com/accessible-website-guide): Link styling, focus indicators, contrast and the other checks that keep a site usable for everyone.
