30 CSS Text Hover Effects03 / 30
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
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 — 2026.3</p>
<span class="cth-03__chip">color → 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 — and the gradient keeps slowly turning while you stay.</p>
</div>
</div>
</section><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 — 2026.3</p>
<span class="cth-03__chip">color → 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 — 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;
}
}@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;
}
}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 — 2026.3</p>
<span class="cth-03__chip">color → 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 — 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;
}
}
```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 — 2026.3</p>
<span class="cth-03__chip">color → 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 — 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:.12sto 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.