30 CSS Text Hover Effects03 / 30

CSS onlyMIT licensed

CSS Text Color Reveal / Background Clip

Solid ink to vivid gradient on hover — with a real crossfade, not the usual hard swap. The trick nobody writes down: keep the gradient painted underneath at all times via background-clip:text, and transition the element's COLOR to transparent — the glyph's solid fill dissolves and the gradient was waiting behind it. Once revealed, two registered @property angles keep the gradient slowly turning, so the text feels lit, not printed. Shown on a SaaS feature index where each heading carries its own --from/--to pair.

Published

Live Demo
Try it

The code

<section class="cth-03" aria-label="Gradient text color reveal demo">
  <div class="cth-03__stage">
    <div class="cth-03__scene">
      <header class="cth-03__head">
        <p class="cth-03__eyebrow">Northlight platform &mdash; 2026.3</p>
        <span class="cth-03__chip">color &rarr; transparent over background-clip:text</span>
      </header>
      <ol class="cth-03__list">
        <li><a class="cth-03__item" style="--from:oklch(0.72 0.19 25);--to:oklch(0.78 0.16 65)" href="#cth03-top" id="cth03-top"><small>001</small>Realtime ledger sync</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.74 0.17 150);--to:oklch(0.78 0.14 210)" href="#cth03-top"><small>002</small>Anomaly-aware alerts</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.66 0.21 300);--to:oklch(0.72 0.19 340)" href="#cth03-top"><small>003</small>Zero-copy exports</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.68 0.17 250);--to:oklch(0.8 0.14 190)" href="#cth03-top"><small>004</small>Region-pinned vaults</a></li>
      </ol>
      <p class="cth-03__hint">Hover or <kbd>Tab</kbd>: solid ink crossfades to a live gradient &mdash; and the gradient keeps slowly turning while you stay.</p>
    </div>
  </div>
</section>
@property --cth03-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 100deg;
}

.cth-03,
.cth-03 *,
.cth-03 *::before,
.cth-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-03 {
  --bg: oklch(0.965 0.005 250);
  --ink: oklch(0.24 0.015 260);
  --mut: oklch(0.54 0.015 255);
  --line: oklch(0.89 0.008 250);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

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

.cth-03__scene {
  width: min(100%,820px);
  display: flex;
  flex-direction: column;
  gap: clamp(18px,3cqi,26px);
}

.cth-03__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}

.cth-03__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-03__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--mut);
  border: 1px solid var(--line);
  border-radius: 99px;
  padding: 5px 11px;
}

.cth-03__list {
  list-style: none;
  counter-reset: cth03;
}

.cth-03__list li {
  border-bottom: 1px solid var(--line);
}

.cth-03__list li:first-child {
  border-top: 1.5px solid var(--ink);
}

.cth-03__item {
  --cth03-angle: 100deg;
  display: flex;
  align-items: baseline;
  gap: clamp(14px,3cqi,26px);
  padding: clamp(14px,2.6cqi,22px) 4px;
  font-size: clamp(26px,5.4cqi,46px);
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.12;
  text-decoration: none;
  background-image: linear-gradient(var(--cth03-angle),var(--from),var(--to));
  -webkit-background-clip: text;
  background-clip: text;
  color: var(--ink);
  transition: color .5s ease,translate .35s cubic-bezier(.22,1,.36,1);
}

.cth-03__item small {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--mut);
  -webkit-text-fill-color: var(--mut);
}

.cth-03__item:hover,
.cth-03__item:focus-visible {
  color: transparent;
  translate: 6px 0;
  animation: cth03-turn 4s ease-in-out .5s infinite alternate;
}

.cth-03__item:focus-visible {
  outline: 2px solid color-mix(in oklch,var(--from) 70%,var(--ink));
  outline-offset: 4px;
  border-radius: 3px;
}

@keyframes cth03-turn {
  from {
    --cth03-angle: 100deg;
  }

  to {
    --cth03-angle: 220deg;
  }
}

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

.cth-03__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);
}

@container cth03 (width < 560px) {
  .cth-03__chip {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cth-03 * {
    transition-duration: .01ms !important;
    animation: none !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 Text Color Reveal / Background Clip
Source: https://codefronts.com/motion/css-text-hover-effects/css-text-color-reveal-background-clip/

Solid ink to vivid gradient on hover — with a real crossfade, not the usual hard swap. The trick nobody writes down: keep the gradient painted underneath at all times via background-clip:text, and transition the element's COLOR to transparent — the glyph's solid fill dissolves and the gradient was waiting behind it. Once revealed, two registered @property angles keep the gradient slowly turning, so the text feels lit, not printed. Shown on a SaaS feature index where each heading carries its own --from/--to pair.
## HTML
```html
<section class="cth-03" aria-label="Gradient text color reveal demo">
  <div class="cth-03__stage">
    <div class="cth-03__scene">
      <header class="cth-03__head">
        <p class="cth-03__eyebrow">Northlight platform &mdash; 2026.3</p>
        <span class="cth-03__chip">color &rarr; transparent over background-clip:text</span>
      </header>
      <ol class="cth-03__list">
        <li><a class="cth-03__item" style="--from:oklch(0.72 0.19 25);--to:oklch(0.78 0.16 65)" href="#cth03-top" id="cth03-top"><small>001</small>Realtime ledger sync</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.74 0.17 150);--to:oklch(0.78 0.14 210)" href="#cth03-top"><small>002</small>Anomaly-aware alerts</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.66 0.21 300);--to:oklch(0.72 0.19 340)" href="#cth03-top"><small>003</small>Zero-copy exports</a></li>
        <li><a class="cth-03__item" style="--from:oklch(0.68 0.17 250);--to:oklch(0.8 0.14 190)" href="#cth03-top"><small>004</small>Region-pinned vaults</a></li>
      </ol>
      <p class="cth-03__hint">Hover or <kbd>Tab</kbd>: solid ink crossfades to a live gradient &mdash; and the gradient keeps slowly turning while you stay.</p>
    </div>
  </div>
</section>
```
## CSS
```css
@property --cth03-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 100deg;
}

.cth-03,
.cth-03 *,
.cth-03 *::before,
.cth-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-03 {
  --bg: oklch(0.965 0.005 250);
  --ink: oklch(0.24 0.015 260);
  --mut: oklch(0.54 0.015 255);
  --line: oklch(0.89 0.008 250);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

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

.cth-03__scene {
  width: min(100%,820px);
  display: flex;
  flex-direction: column;
  gap: clamp(18px,3cqi,26px);
}

.cth-03__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}

.cth-03__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-03__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--mut);
  border: 1px solid var(--line);
  border-radius: 99px;
  padding: 5px 11px;
}

.cth-03__list {
  list-style: none;
  counter-reset: cth03;
}

.cth-03__list li {
  border-bottom: 1px solid var(--line);
}

.cth-03__list li:first-child {
  border-top: 1.5px solid var(--ink);
}

.cth-03__item {
  --cth03-angle: 100deg;
  display: flex;
  align-items: baseline;
  gap: clamp(14px,3cqi,26px);
  padding: clamp(14px,2.6cqi,22px) 4px;
  font-size: clamp(26px,5.4cqi,46px);
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.12;
  text-decoration: none;
  background-image: linear-gradient(var(--cth03-angle),var(--from),var(--to));
  -webkit-background-clip: text;
  background-clip: text;
  color: var(--ink);
  transition: color .5s ease,translate .35s cubic-bezier(.22,1,.36,1);
}

.cth-03__item small {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--mut);
  -webkit-text-fill-color: var(--mut);
}

.cth-03__item:hover,
.cth-03__item:focus-visible {
  color: transparent;
  translate: 6px 0;
  animation: cth03-turn 4s ease-in-out .5s infinite alternate;
}

.cth-03__item:focus-visible {
  outline: 2px solid color-mix(in oklch,var(--from) 70%,var(--ink));
  outline-offset: 4px;
  border-radius: 3px;
}

@keyframes cth03-turn {
  from {
    --cth03-angle: 100deg;
  }

  to {
    --cth03-angle: 220deg;
  }
}

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

.cth-03__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);
}

@container cth03 (width < 560px) {
  .cth-03__chip {
    display: none;
  }
}

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

How this works

background-image cannot transition, and that's what breaks every naive attempt. So never transition the gradient — transition what's covering it:

.reveal{
  background-image:linear-gradient(var(--cth03-angle),var(--from),var(--to));
  -webkit-background-clip:text; background-clip:text;
  color:var(--ink);                 /* solid glyphs COVER the clip */
  transition:color .5s ease; }
.reveal:hover,
.reveal:focus-visible{ color:transparent; }   /* uncover it */

The clipped gradient is painted from the first frame; the element's currentColor is simply a layer above it. Fading color to transparent is a true crossfade — antialiasing stays clean because the browser is compositing two fills of the same glyph mask, and mouse-out reverses it symmetrically. This also future-proofs the markup: the fallback for engines without background-clip:text is just… the ink-colored text.

The second act is why it feels alive: --cth03-angle is registered as a typed <angle>, and a keyframe rotates it 120deg back and forth while hovered — the browser interpolates the custom property, re-resolving the gradient every frame. That's the @property superpower: gradients become animatable through their inputs. Each list item overrides only --from/--to, so one rule serves the whole index in different hues.

Make it yours

  • Per-item hues are two custom properties: style='--from:oklch(0.75 0.18 150);--to:oklch(0.7 0.17 210)' — pick neighbors on the hue wheel for silk, opposites for punch.
  • Slow the reveal to .8s and add transition-delay:.12s to forgive drive-by cursor grazes on dense lists.
  • Want the gradient to travel instead of rotate? Set background-size:220% 100% and animate background-position 0%→100% while hovered — a lateral sheen instead of a turn.
  • Dark-mode flip: keep --from/--to, swap --ink and the page background — the crossfade mechanic is palette-agnostic.

Gotchas — read before shipping

  • Both prefixes, always: -webkit-background-clip:text then background-clip:text. Miss the prefixed one and Chromium paints the gradient as a full background rectangle behind the text.
  • Anything that paints ON the element (text-shadow, text-decoration) sits above the clipped gradient and ruins the reveal — put underlines on a pseudo-element or wrapper instead.
  • padding on the clipped element extends the gradient beyond the glyphs harmlessly, but border does not: background-clip:text clips to glyphs only; borders need a wrapper.
  • The angle keyframe holds a compositor-cheap look but IS a repaint per frame (gradient re-raster). Scope it to :hover — don't idle-spin every heading on the page.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The color→transparent crossfade over clipped gradients works in every 2020+ engine. The rotating-angle flourish needs @property (Chrome 85+/Safari 16.4+/Firefox 128+) — without it the gradient reveals but holds still. oklch() floors the palette at 2023 engines; swap hex to go older.

Techniques used in this demo

Search CodeFronts

Loading…