30 CSS Text Hover Effects01 / 30

CSS onlyMIT licensed

CSS Animated Underline on Hover

The four underline animations every navbar ships in 2026, side by side: grow left→right, bloom from center, slide-through (the line exits the far side instead of retreating), and the pure text-decoration version that needs no pseudo-element at all. The first three are one background-image gradient sized from 0%→100% — cheaper than ::after, works on inline links that wrap — and the fourth animates text-decoration-color, thickness and underline-offset directly, which finally transitions in every modern engine.

Published

Live Demo
Try it

The code

<section class="cth-01" aria-label="Animated underline on hover demo">
  <div class="cth-01__stage">
    <div class="cth-01__scene">
      <header class="cth-01__masthead">
        <a class="cth-01__brand" href="#cth01-top" id="cth01-top">Ledger<span>&amp;</span>Loom</a>
        <span class="cth-01__chip">background-size: 0% &rarr; 100%</span>
      </header>
      <nav class="cth-01__nav" aria-label="Underline animation styles">
        <div class="cth-01__row">
          <span class="cth-01__tag">01 &middot; left &rarr; right</span>
          <a class="cth-01__link cth-01__link--ltr" href="#cth01-top">Selected work</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">02 &middot; center out</span>
          <a class="cth-01__link cth-01__link--center" href="#cth01-top">Studio journal</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">03 &middot; slide through</span>
          <a class="cth-01__link cth-01__link--thru" href="#cth01-top">About the practice</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">04 &middot; text-decoration</span>
          <a class="cth-01__link cth-01__link--native" href="#cth01-top">Get in touch</a>
        </div>
      </nav>
      <p class="cth-01__prose">Underlines also live inside prose: the <a href="#cth01-top">type specimens</a> and the <a href="#cth01-top">2026 grid audit</a> use the same gradient at marker weight &mdash; and because they're real inline links, they keep underlining when they wrap.</p>
      <p class="cth-01__hint">Hover the links &mdash; or press <kbd>Tab</kbd>. Every variant also fires on <code>:focus-visible</code>.</p>
    </div>
  </div>
</section>
.cth-01,
.cth-01 *,
.cth-01 *::before,
.cth-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-01 {
  --bg: oklch(0.975 0.007 95);
  --ink: oklch(0.25 0.02 55);
  --mut: oklch(0.52 0.02 60);
  --acc: oklch(0.60 0.19 33);
  --line: oklch(0.89 0.012 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-01__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: var(--bg);
  padding: clamp(20px,4vw,56px);
  container: cth01/inline-size;
}

.cth-01__scene {
  width: min(100%,780px);
  display: flex;
  flex-direction: column;
  gap: clamp(20px,3.5cqi,30px);
}

.cth-01__masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 16px;
  border-bottom: 1.5px solid var(--ink);
}

.cth-01__brand {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: inherit;
  text-decoration: none;
}

.cth-01__brand span {
  color: var(--acc);
}

.cth-01__brand:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 4px;
}

.cth-01__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  font-weight: 600;
  color: var(--acc);
  border: 1px solid color-mix(in oklch,var(--acc) 45%,transparent);
  border-radius: 99px;
  padding: 5px 11px;
  white-space: nowrap;
}

.cth-01__row {
  display: grid;
  grid-template-columns: 170px 1fr;
  gap: 6px 24px;
  align-items: baseline;
  padding: clamp(12px,2.2cqi,18px) 0;
  border-bottom: 1px solid var(--line);
}

.cth-01__tag {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-01__link {
  justify-self: start;
  font-size: clamp(24px,4.6cqi,38px);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.2;
  color: inherit;
  text-decoration: none;
  padding-bottom: .12em;
}

.cth-01__link:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 5px;
  border-radius: 2px;
}

.cth-01__link--ltr,
.cth-01__link--center,
.cth-01__link--thru {
  background-image: linear-gradient(var(--acc),var(--acc));
  background-repeat: no-repeat;
  background-size: 0% .09em;
  transition: background-size .45s cubic-bezier(.22,1,.36,1);
}

.cth-01__link--ltr {
  background-position: 0% 100%;
}

.cth-01__link--center {
  background-position: 50% 100%;
}

.cth-01__link--thru {
  background-position: 100% 100%;
}

.cth-01__link--ltr:hover,
.cth-01__link--ltr:focus-visible,
.cth-01__link--center:hover,
.cth-01__link--center:focus-visible {
  background-size: 100% .09em;
}

.cth-01__link--thru:hover,
.cth-01__link--thru:focus-visible {
  background-position: 0% 100%;
  background-size: 100% .09em;
}

.cth-01__link--native {
  text-decoration-line: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: .06em;
  text-underline-offset: .32em;
  text-decoration-skip-ink: auto;
  transition: text-decoration-color .35s ease,text-underline-offset .35s cubic-bezier(.22,1,.36,1),text-decoration-thickness .35s ease;
}

.cth-01__link--native:hover,
.cth-01__link--native:focus-visible {
  text-decoration-color: var(--acc);
  text-underline-offset: .14em;
  text-decoration-thickness: .1em;
}

.cth-01__prose {
  font-size: 15px;
  line-height: 1.75;
  color: var(--mut);
  max-width: 58ch;
}

.cth-01__prose a {
  color: var(--ink);
  font-weight: 650;
  text-decoration: none;
  background-image: linear-gradient(color-mix(in oklch,var(--acc) 32%,transparent),color-mix(in oklch,var(--acc) 32%,transparent));
  background-repeat: no-repeat;
  background-position: 0 88%;
  background-size: 100% .14em;
  transition: background-size .3s ease;
}

.cth-01__prose a:hover,
.cth-01__prose a:focus-visible {
  background-size: 100% .6em;
}

.cth-01__prose a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.cth-01__hint {
  font-size: 12.5px;
  color: var(--mut);
}

.cth-01__hint kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  background: oklch(1 0 0);
}

.cth-01__hint code {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .92em;
  background: oklch(0.93 0.01 95);
  padding: 1px 5px;
  border-radius: 4px;
}

@container cth01 (width < 560px) {
  .cth-01__row {
    grid-template-columns: 1fr;
  }

  .cth-01__chip {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cth-01 * {
    transition-duration: .01ms !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Text Hover Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Animated Underline on Hover
Source: https://codefronts.com/motion/css-text-hover-effects/css-animated-underline-on-hover/

The four underline animations every navbar ships in 2026, side by side: grow left→right, bloom from center, slide-through (the line exits the far side instead of retreating), and the pure text-decoration version that needs no pseudo-element at all. The first three are one background-image gradient sized from 0%→100% — cheaper than ::after, works on inline links that wrap — and the fourth animates text-decoration-color, thickness and underline-offset directly, which finally transitions in every modern engine.
## HTML
```html
<section class="cth-01" aria-label="Animated underline on hover demo">
  <div class="cth-01__stage">
    <div class="cth-01__scene">
      <header class="cth-01__masthead">
        <a class="cth-01__brand" href="#cth01-top" id="cth01-top">Ledger<span>&amp;</span>Loom</a>
        <span class="cth-01__chip">background-size: 0% &rarr; 100%</span>
      </header>
      <nav class="cth-01__nav" aria-label="Underline animation styles">
        <div class="cth-01__row">
          <span class="cth-01__tag">01 &middot; left &rarr; right</span>
          <a class="cth-01__link cth-01__link--ltr" href="#cth01-top">Selected work</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">02 &middot; center out</span>
          <a class="cth-01__link cth-01__link--center" href="#cth01-top">Studio journal</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">03 &middot; slide through</span>
          <a class="cth-01__link cth-01__link--thru" href="#cth01-top">About the practice</a>
        </div>
        <div class="cth-01__row">
          <span class="cth-01__tag">04 &middot; text-decoration</span>
          <a class="cth-01__link cth-01__link--native" href="#cth01-top">Get in touch</a>
        </div>
      </nav>
      <p class="cth-01__prose">Underlines also live inside prose: the <a href="#cth01-top">type specimens</a> and the <a href="#cth01-top">2026 grid audit</a> use the same gradient at marker weight &mdash; and because they're real inline links, they keep underlining when they wrap.</p>
      <p class="cth-01__hint">Hover the links &mdash; or press <kbd>Tab</kbd>. Every variant also fires on <code>:focus-visible</code>.</p>
    </div>
  </div>
</section>
```
## CSS
```css
.cth-01,
.cth-01 *,
.cth-01 *::before,
.cth-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-01 {
  --bg: oklch(0.975 0.007 95);
  --ink: oklch(0.25 0.02 55);
  --mut: oklch(0.52 0.02 60);
  --acc: oklch(0.60 0.19 33);
  --line: oklch(0.89 0.012 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-01__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: var(--bg);
  padding: clamp(20px,4vw,56px);
  container: cth01/inline-size;
}

.cth-01__scene {
  width: min(100%,780px);
  display: flex;
  flex-direction: column;
  gap: clamp(20px,3.5cqi,30px);
}

.cth-01__masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 16px;
  border-bottom: 1.5px solid var(--ink);
}

.cth-01__brand {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: inherit;
  text-decoration: none;
}

.cth-01__brand span {
  color: var(--acc);
}

.cth-01__brand:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 4px;
}

.cth-01__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  font-weight: 600;
  color: var(--acc);
  border: 1px solid color-mix(in oklch,var(--acc) 45%,transparent);
  border-radius: 99px;
  padding: 5px 11px;
  white-space: nowrap;
}

.cth-01__row {
  display: grid;
  grid-template-columns: 170px 1fr;
  gap: 6px 24px;
  align-items: baseline;
  padding: clamp(12px,2.2cqi,18px) 0;
  border-bottom: 1px solid var(--line);
}

.cth-01__tag {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-01__link {
  justify-self: start;
  font-size: clamp(24px,4.6cqi,38px);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.2;
  color: inherit;
  text-decoration: none;
  padding-bottom: .12em;
}

.cth-01__link:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 5px;
  border-radius: 2px;
}

.cth-01__link--ltr,
.cth-01__link--center,
.cth-01__link--thru {
  background-image: linear-gradient(var(--acc),var(--acc));
  background-repeat: no-repeat;
  background-size: 0% .09em;
  transition: background-size .45s cubic-bezier(.22,1,.36,1);
}

.cth-01__link--ltr {
  background-position: 0% 100%;
}

.cth-01__link--center {
  background-position: 50% 100%;
}

.cth-01__link--thru {
  background-position: 100% 100%;
}

.cth-01__link--ltr:hover,
.cth-01__link--ltr:focus-visible,
.cth-01__link--center:hover,
.cth-01__link--center:focus-visible {
  background-size: 100% .09em;
}

.cth-01__link--thru:hover,
.cth-01__link--thru:focus-visible {
  background-position: 0% 100%;
  background-size: 100% .09em;
}

.cth-01__link--native {
  text-decoration-line: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: .06em;
  text-underline-offset: .32em;
  text-decoration-skip-ink: auto;
  transition: text-decoration-color .35s ease,text-underline-offset .35s cubic-bezier(.22,1,.36,1),text-decoration-thickness .35s ease;
}

.cth-01__link--native:hover,
.cth-01__link--native:focus-visible {
  text-decoration-color: var(--acc);
  text-underline-offset: .14em;
  text-decoration-thickness: .1em;
}

.cth-01__prose {
  font-size: 15px;
  line-height: 1.75;
  color: var(--mut);
  max-width: 58ch;
}

.cth-01__prose a {
  color: var(--ink);
  font-weight: 650;
  text-decoration: none;
  background-image: linear-gradient(color-mix(in oklch,var(--acc) 32%,transparent),color-mix(in oklch,var(--acc) 32%,transparent));
  background-repeat: no-repeat;
  background-position: 0 88%;
  background-size: 100% .14em;
  transition: background-size .3s ease;
}

.cth-01__prose a:hover,
.cth-01__prose a:focus-visible {
  background-size: 100% .6em;
}

.cth-01__prose a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.cth-01__hint {
  font-size: 12.5px;
  color: var(--mut);
}

.cth-01__hint kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  background: oklch(1 0 0);
}

.cth-01__hint code {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .92em;
  background: oklch(0.93 0.01 95);
  padding: 1px 5px;
  border-radius: 4px;
}

@container cth01 (width < 560px) {
  .cth-01__row {
    grid-template-columns: 1fr;
  }

  .cth-01__chip {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cth-01 * {
    transition-duration: .01ms !important;
  }
}
```

How this works

Forget border-bottom (it jumps layout) and skip ::after (it breaks on wrapped inline links). Paint the underline as a background gradient and animate its size — the glyphs never move, and a link that wraps across two lines underlines both:

.link{ background-image:linear-gradient(var(--acc),var(--acc));
  background-repeat:no-repeat;
  background-size:0% .09em;          /* invisible line, em-scaled   */
  background-position:0% 100%;       /* anchored bottom-left        */
  transition:background-size .45s cubic-bezier(.22,1,.36,1); }
.link:hover,
.link:focus-visible{ background-size:100% .09em; }

The anchor point is the whole trick. background-position:0% 100% grows the line from the left; 50% 100% blooms it from the center; and the slide-through variant parks the anchor at 100% 100% then swaps it to 0% 100% inside the hover rule — position isn't in the transition list, so the swap is instant and invisible while the size tween makes the line enter from the left and, on mouse-out, exit right. One property animated, three personalities.

The fourth variant is the 2026-native path: real text-decoration-line:underline with text-decoration-color fading in while text-underline-offset tightens from .32em to .14em — the line visibly docks onto the text. It respects descenders via text-decoration-skip-ink, something no gradient can do. Sizing everything in em means the same class works on a 13px footer link and a 38px menu item without retuning.

Make it yours

  • Line weight and gap are the two knobs: the .09em in background-size is the thickness; add padding-bottom:.15em to push the line below descenders.
  • Highlighter mode: raise the height from .09em to .55em and drop the gradient to 35% opacity with color-mix — the underline becomes a marker swipe (the inline prose links in this demo do exactly that).
  • Two-tone draw: stack two gradients with offset background-positions and stagger their transitions 80ms apart for a chased, hand-drawn feel.
  • Speed discipline: 300–500ms with an ease-out feels editorial; under 200ms reads as a state flip, not an animation.

Gotchas — read before shipping

  • display:inline-block on a link kills multi-line wrapping AND gradient underlines across breaks — keep links display:inline and let background-repeat handle wrapped lines (that's the point of this technique).
  • transition:all also tweens the background-position swap in the slide-through variant, breaking the exit illusion — transition background-size only.
  • text-decoration-thickness/offset transitions need Chrome 117+/Safari 16.4+; older engines snap between states (still legible, just not animated) — fine to ship, know what degrades.
  • A 2px hairline on a 44px target is a thin affordance for touch — pair underlines with :focus-visible outlines (this demo does) so keyboard users get more than a fading line.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Gradient-underline variants are 2017-era CSS and run anywhere; the floor comes from oklch()/color-mix() tokens (swap for hex to go older). Variant 04's animated text-underline-offset needs Chrome 117+/Safari 16.4+; it degrades to an instant swap elsewhere.

Techniques used in this demo

Search CodeFronts

Loading…