Hover animations for text links with HTML and CSS

132

Creating hover states and animations for text is something most websites end up doing in one form or another.

If the only goal is to show that the text is a link, a simple underline like the browser default may be enough.

But when you want the change to be easier to notice, or you want it to match the overall tone of the site, a small animation can be very effective.

This article introduces a set of simple yet slightly eye-catching animations that can be built with HTML and CSS alone.

Overview of the demos in this article

This article will be especially useful for designers and engineers who:

  • want to know what kinds of hover animations can be created with HTML and CSS alone
  • want more ideas beyond opacity changes and simply adding or removing an underline

Note: These demos mainly assume relatively short link text, such as the kind used in headers, footers, or side navigation, where the text fits on a single line.

Common HTML

Before getting into the demos, here is the HTML shared by most of them. It is a simple structure with a single a tag.

Only “4. Text with perspective” uses different HTML. That section includes its own HTML together with the CSS.

HTML for the link text

<a href="#" class="text">LINK TEXT</a>

Common CSS

Next, here is the CSS shared by all of the demos. It simply removes the browser’s default link styling.

CSS for the link text

a {
  color: #000;
  text-decoration: none;
}

This is not central to the article, but styles used only for presentation, such as centering the link text on screen, are written in a separate layout CSS file. See the following for details.

From here, each demo is introduced one by one.

1. Wider letter spacing

Try hovering over it.

CSS excerpt for the link text

.text {
  font-size: 60px;
  transition: letter-spacing 0.3s;
}

.text:hover {
  letter-spacing: 0.05em; /* Letter spacing */
}

Explanation

This is a simple animation that widens the spacing between characters by using the letter-spacing property.

The unit uses em, which is a relative value. em depends on the font size applied to the element itself. In this case, because font-size: 60px; is specified, 1em becomes 60px. That means 0.05em becomes 60 × 0.05 = 3, so it is effectively the same as letter-spacing: 3px;.

Using em has the following advantages:

  • Unlike an absolute unit such as px, it does not need to be fine-tuned again whenever the font size changes
  • It preserves a similar visual impression on hover even when applied to text with different font sizes

One thing to keep in mind is that the width of each character varies by font.

2. Text rotates vertically

Try hovering over it.

CSS excerpt

.text {
  overflow: hidden; /* Hide the first shadow */
  color: transparent; /* Make the text itself transparent */
  font-size: 60px;
  /*
   Shadow settings
   1st shadow: positioned above the text with a negative Y offset, with no blur
   2nd shadow: positioned in the same place as the text itself, with no blur
  */
  text-shadow: 0 -1.5em 0 #000, 0 0 0 #000;
  transition: text-shadow 0.3s;
}

.text:hover {
  text-shadow: 0 0 0 #000, 0 1.5em 0 #000; /* Shift the two shadow positions by 1.5em */
}

Explanation

If you look closely at the animation, two copies of the text appear at the same time. Both of them are text shadows created with the text-shadow property. Because text-shadow accepts multiple comma-separated values, the structure here is simply two shadows stacked vertically.

The first shadow is hidden by overflow: hidden;.

If you turn off overflow: hidden; in your browser’s developer tools and watch the motion again, the mechanism becomes much easier to understand.

3. Text rotates vertically with a background color

This is a variation of the previous demo, “2. Text rotates vertically,” with a background color added.

Try hovering over it.

CSS excerpt for the link text

.text {
  padding: 0 10px;
  /* Hide the first shadow */
  overflow: hidden;
  color: transparent; /* Make the text itself transparent */
  font-size: 60px;
  /*
   Shadow settings
   1st shadow: positioned above the text with a negative Y offset, with no blur
   2nd shadow: positioned in the same place as the text itself, with no blur
  */
  text-shadow: 0 -1.5em 0 #000, 0 0 0 #000;
  background: linear-gradient(to bottom, #000 50%, transparent 50%) 0 100%;
  background-size: 100% 200%;
  transition: text-shadow 0.3s, background-position 0.3s;
}

.text:hover {
  /* Shift the two shadow positions by 1.5em */
  text-shadow: 0 0 0 #fff, 0 1.5em 0 #000;

  /* Move the background position */
  background-position: 0 0;
}

Explanation

The linear-gradient() function is used so that the upper half of the background is black and the lower half is transparent. The values inside to bottom, #000 50%, transparent 50% mean the following:

  • to bottom: the gradient changes from top to bottom, creating a vertical gradient
  • #000 50%: the area from 0 to 50% (the upper half) is black
  • transparent 50%: the area from 50 to 100% (the lower half) is transparent

Because black and transparent are both specified at the same 50% position, the color switches sharply instead of blending gradually.

The size is set with background-size to 100% wide and 200% high. Because of overflow: hidden;, the black upper half of the background is hidden in the initial state.

On hover, the background-position property is adjusted so that the black background appears to rotate down together with the first shadow of the text.

4. Text with perspective

Try hovering over it.

HTML excerpt for the link text

<a href="#" class="text"><span>LINK TEXT</span></a>

Unlike the other demos, the text is wrapped in a span tag.

CSS excerpt for the link text

.text span {
  /* transform does not work on the initial inline display type, so use inline-block */
  display: inline-block;
  font-size: 60px;
  transition: transform 0.3s;
}

.text:hover span {
  transform: perspective(600px) rotateY(-15deg) rotateX(20deg);
}

Explanation

This demo uses the perspective() function inside the transform property to add depth. Even without perspective(), the text would still tilt with rotateY() and rotateX(), but adding perspective makes the result feel more three-dimensional.

The smaller the value passed to perspective(), the stronger the sense of depth becomes, so it is worth experimenting with in your developer tools.

In this demo, the styles are applied not to the a tag with the text class, but to the child span tag. That prevents the clickable area from changing when transform is applied. If the a tag itself is transformed here, the clickable area changes between the normal state and the hovered state.

5. A circle appears from the left

Try hovering over it.

CSS excerpt

.text {
  display: flex; /* Place the text and pseudo-element side by side */
  font-size: 60px;
  align-items: center; /* Align vertically in the center */
}

.text::before {
  width: 0; /* Hide it in the initial state */
  height: 0;
  content: "";
  background-color: transparent;
  border-radius: 50%;
  transition: 0.3s;
}

.text:hover::before {
  width: 0.25em; /* Show it on hover */
  height: 0.25em;
  margin-right: 36px;
  background-color: #000;
}

Explanation

Because the circle appears and the text moves at the same time, the effect draws the eye more strongly. It can be especially useful when multiple links are arranged vertically, because the hovered item becomes easier to distinguish.

The circle is created with a pseudo-element. In the initial state, width and height are set to 0 so that it remains hidden and does not affect the layout.

Because of that, background-color: transparent; may look unnecessary at first. However, it is included so the color can transition smoothly from transparent to #000 when the element appears.

6. An underline sweeps in from the left

Try hovering over it.

CSS excerpt

.text {
  padding-bottom: 3px; /* Space between the text and underline */
  font-size: 60px;
  background-image: linear-gradient(#000, #000);
  background-repeat: no-repeat;
  background-position: bottom right; /* Initial underline position */
  background-size: 0 1px; /* Underline size (width, height) */
  transition: background-size 0.3s;
}

.text:hover {
  background-position: bottom left; /* Underline position on hover */
  background-size: 100% 1px; /* Make the underline 100% wide */
}

Explanation

The background-image and background-size properties create a 1px-high line. Then, by changing the value of background-position, the line appears to sweep in from the left.

Changing the direction of the underline

A small change to the value of background-position lets you change the way the underline moves.

Make it sweep from right to left (only the changed lines are shown)

.text {
  background-position: bottom left; /* Initial underline position */
}

.text:hover {
  background-position: bottom right; /* Underline position on hover */
}

Start from the left and return to the left (only the changed lines are shown)

.text {
  background-position: bottom left; /* Initial underline position */
}

.text:hover {
  background-position: bottom left; /* Underline position on hover */
}

There are many ways to draw an underline

The following article covers a variety of underline techniques in more detail, including how to choose an approach depending on the situation and how to support multi-line text.

7. Text color changes from the left

Try hovering over it.

CSS excerpt for the link text

.text {
  /* Make the text transparent so the background shows through */
  color: transparent;
  font-size: 60px;

  /* A gradient where orange and black switch at 50% */
  background: linear-gradient(to right, orange 50%, #000 50%) 100%;
  /* Clip the background to the text */
  background-clip: text;

  /* Make it 200% wide so the orange portion stays hidden */
  background-size: 200% 100%;

  transition: background-position 0.3s;
}

.text:hover {
  /* Move the gradient to reveal the orange portion */
  background-position: 0 100%;
}

Explanation

Conceptually, this works like a background twice as wide as the text (200%) sliding behind the text.

As mentioned earlier in “3. Text rotates vertically with a background color,” the gradient switches sharply because the two color stops share the same percentage position inside linear-gradient().

Also, setting background-clip to text clips the background to the shape of the text itself.

8. Text and background colors change from the left

Try hovering over it.

CSS excerpt for the link text

.text {
  position: relative;
  padding: 0 10px; /* Padding for visual adjustment */
  color: #000;
  font-size: 60px;
  background-color: #fff;
}

.text::before {
  position: absolute;
  left: 0;
  width: 0;
  height: 100%;
  content: "";
  background-color: #fff;
  mix-blend-mode: difference; /* difference */
  transition: 0.3s;
}

.text:hover::before {
  width: 100%;
}

Explanation

This demo uses the mix-blend-mode property to change the way overlapping elements are displayed. The sweeping background is created with a pseudo-element. The implementation itself is simple: the width starts at 0 and expands to 100% on hover.

The key is mix-blend-mode: difference;.

The mix-blend-mode property changes the way overlapping elements appear. Here, the pseudo-element uses difference, so the overlapping area shows the difference between the colors.

As a result, when the white pseudo-element moves across the element, the white background area appears black, while the black text appears white.

9. Text and background colors change from the left over an image background

With only black and white, the result looks like a simple color inversion. But the same effect can also be interesting when layered over elements that contain many colors, such as an image.

The following demo prepares a background image and also applies mix-blend-mode to the text itself.

Try hovering over it.

CSS excerpt for the background image and link text

body {
  background-image: url("./images/bg.jpg");
  background-size: cover;
}

/* Text and background colors change from the left */
.text {
  position: relative;
  padding: 0 10px; /* Padding for visual adjustment */
  color: #fff;
  font-size: 60px;
  mix-blend-mode: difference; /* difference */
}

.text::before {
  position: absolute;
  left: 0;
  width: 0;
  height: 100%;
  content: "";
  background-color: white;
  mix-blend-mode: difference; /* difference */
  transition: 0.3s;
}

.text:hover::before {
  width: 100%;
}

10. The text blurs for a moment

Try hovering over it.

CSS excerpt for the link text

.text {
  font-size: 60px;
}

.text:hover {
  animation: text-blur 0.5s;
}

/* Animation settings */
@keyframes text-blur {
  0% {
    filter: blur(0);
  }
  50% {
    filter: blur(4px); /* Blur amount */
  }
  100% {
    filter: blur(0);
  }
}

Explanation

This effect uses the @keyframes at-rule so that the text blurs on hover and then returns to normal. Because blurred text becomes harder to read, the duration of the change is kept short.

The blur itself is created with the blur() function inside the filter property. A value of 0 means “no blur,” and larger values produce a stronger blur.

11. The background behind the text blurs

Try hovering over it.

CSS excerpt for the link text

.text {
  position: relative;
  padding: 0 20px;
  color: #fff8;
  font-size: 60px;
}

/* Pseudo-element for the text */
.text::before {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1; /* Display it behind the text */
  width: 100%;
  height: 100%;
  content: "";
  transition: 0.3s;
}

/* Pseudo-element when the text is hovered */
.text:hover::before {
  -webkit-backdrop-filter: blur(10px); /* For Safari */
  backdrop-filter: blur(10px); /* Blur the background */
}

Explanation

This is a particularly effective technique when there is a background element such as an image.

In the previous demo, “The text blurs for a moment,” the filter property blurred the element itself. In contrast, the backdrop-filter property used here blurs the content behind the element.

Browser support for the backdrop-filter property

It is supported in most modern browsers, but as of August 2024, Safari still required a vendor prefix. Starting with Safari 18.0, scheduled for release in fall 2024, the vendor prefix will no longer be necessary.

Additional tips

The following sections introduce a few things worth adjusting when applying these patterns to a real site.

Column: Adjusting animation easing

In the demos introduced above, the transition property was written with generic values that only specify the animated property and duration. When using these patterns in an actual site, try adjusting the easing function so the motion matches the rest of the interface and the overall tone of the site.

As one example, here is the “Text rotates vertically” demo adjusted with the cubic-bezier() function. Both the original and adjusted versions are shown below so the difference is easier to compare.

Try hovering over it (before adjustment)

Try hovering over it (after adjustment)

Compared with the original version, the motion has a little more afterglow and feels slightly more refined and polished.

In the code, the following declaration was added to the CSS.

Only the added CSS is shown below.

.text {
  /*
  Use cubic-bezier to add stronger contrast to the easing
  easeInOutQuart easing
  */
  transition: text-shadow 0.4s cubic-bezier(0.76, 0, 0.24, 1);
}

For a deeper look at easing, including how to use cubic-bezier() and how to make motion feel smoother, see the following article.

Column: What if you do not want hover animations on mobile devices?

Not only on mobile devices, but on touch devices in general, hover animations triggering when a user taps are usually unintended.

Even if desktop and mobile styles are switched by screen width, it is still a problem if larger touch devices trigger hover animation on tap.

In that case, use the any-hover media feature. A media feature is the part of @media syntax that describes the device environment and related conditions. With any-hover: hover, you can check whether the device supports hover interaction.

Using the first demo, “Wider letter spacing,” as a base, it would look like this:

/* Apply only on devices that support hover */
@media (any-hover: hover) {
  .text:hover {
    letter-spacing: 0.05em;
  }
}

The any-hover media feature is also covered in the following articles:

Conclusion

This article introduced hover animations for link text. Most of them are subtle and simple on their own, but combining multiple animations could also produce a very different impression. Hopefully these demos give you ideas to experiment with.

The following article introduces text animations where each character changes individually. The HTML is a bit more complex than in this article, but it opens up a wider range of expression, so it is also worth a look.

ICS MEDIA also has many other articles that can help expand your set of UI implementation ideas. These are worth checking as well.

Share on social media
Your shares help us keep the site running.
Post on X
Post to Hatena Bookmark
Share
Copy URL
SAWADA Naomi

Joined ICS as a front-end engineer after working as a web designer. Loves drawing, games, music, movies, and comedy.

Articles by this staff