Saturday, July 25, 2026

Sliding color change underline on hover

I wanted this effect for the in-text links on my website, but I could not find a ready-made solution that did what I wanted. Here is a short tutorial.

Skip the chit-chat, here’s the code:

This style transitions from black to green like so: Example link

a {
  display: inline-block;
  position: relative;
  clip-path: inset(-5px 0);
  paint-order: stroke fill;
  -webkit-text-stroke: 0.3em var(--background-color);
}

a::after {
  content: "";
  position: absolute;
  bottom: -1px;
  left: 0;
  width: 250%;
  height: 1px;
  background: linear-gradient(
    to right,
    limegreen,
    limegreen 50%,
    black 50%,
    black
  );
  transition: transform 250ms ease-in-out;
  transform: translateX(-50%);
  z-index: -1;
}

a:hover::after {
  transform: translateX(0);
}

With the chit-chat

Sure, I could have used an LLM for this, there is no fun in that. The CSS is quite simple in the end, but there are a few little tricks. Hopefully, this helps somebody learn a thing or two!

Gradient trick

To use two colors, we create a gradient with overlapping color stops—two colors at the same point in the linear-gradient, creating a sharp transition, and then translate this two-colored bar on hover. Another trick is to make the bar wider than 200% so we don’t get a leftover pixel on either side.

Here is an example of what the animated underline with the gradient looks like:

This underline is nice, but it's leaking!

display: inline vs inline-block

Now we need to hide the overflowing gradient outside our link. So overflow: hidden, right?

The problem is that we need display: inline-block for overflow: hidden to actually work. But that combination changes the link’s baseline and limits where the underline can be positioned, pushing it too far from the text to remain visible.

Edit 2026-08-11: vertical-align could fix the baseline alignment, but we would still have the cropping problem.

Lorem ipsum dolor sit BADABIM! amet

Hiding the overflow with clip-path: inset()

Another way to hide overflow is clip-path, which is much more flexible: you can essentially use SVG shapes to draw anything you want.

It also provides us with a useful function, inset(), which “defines a rectangle at the specified inset distances from each side of the reference box” MDN.

By using clip-path: inset(-5px 0);, we cut off horizontal overflow to the left and right (0) while allowing slight vertical room (-5px) so the underline thickness doesn’t get clipped on top or bottom. We still keep display: inline-block. Safari has trouble clipping regular inline boxes, but an inline block gives it one clean box to clip without the baseline problem caused by overflow: hidden.

Arf! descenders

eh paf! The underline touches the bottom of the p.

There are two ways to approach this. We could position the underline lower, but I’m not fond of that option. Or we can try to reproduce text-decoration-skip-ink, which I prefer. For this, we use -webkit-text-stroke, which allows us to draw an outline of our text in the same color as the background, effectively “eating” the underline. We also use paint-order: stroke fill; to have the stroke grow outside the text.

That’s it!

Here is the final result working smoothly:

Try hovering over this smooth sliding underline link in action!