Pure CSSMIT licensed

CSS Text Fill & Color Wave Hover Effect

Liquid typography: ink that rises inside the glyphs when you hover. A flat fill that climbs from the baseline, a wavy crest version that actually looks like liquid (an inline SVG wave, no canvas), and a color wave that flows through the letters for as long as the pointer stays. All three are one gradient trick wearing different costumes.

Live Demo
Try it

Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS

ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

The code

<section class="cle-05" aria-label="Text fill and color wave hover demo">
  <div class="cle-05__stage">
    <div class="cle-05__var"><a href="#a" class="cle-05__rise">Reservoir</a><small class="cle-05__cap">A · flat fill rises</small></div>
    <div class="cle-05__var"><a href="#b" class="cle-05__wave">High tide</a><small class="cle-05__cap">B · liquid crest (SVG wave)</small></div>
    <div class="cle-05__var"><a href="#c" class="cle-05__flow">Spectrum</a><small class="cle-05__cap">C · color wave while hovered</small></div>
  </div>
</section>
.cle-05,
.cle-05 *,
.cle-05 *::before,
.cle-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-05 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.26 0.02 240);
  --brand: oklch(0.58 0.14 230);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-05__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 42px;
  border: 1px solid oklch(0.9 0.012 240);
  background: linear-gradient(180deg,oklch(0.985 0.005 220),oklch(0.945 0.015 230));
  padding: 44px 32px;
}

.cle-05__var {
  display: grid;
  gap: 12px;
  justify-items: center;
}

.cle-05__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.03 235);
}

.cle-05 a {
  font-size: clamp(34px,5.6vw,52px);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.1;
  text-decoration: none;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-repeat: repeat-x;
}

.cle-05 a::selection {
  background: oklch(0.85 0.06 230 / .5);
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
}

.cle-05 a:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 6px;
  border-radius: 4px;
}

.cle-05__rise {
  background-image: linear-gradient(to top,var(--brand) 0 50%,var(--ink) 50% 100%);
  background-size: 100% 200%;
  background-position: 0 0;
  transition: background-position .55s cubic-bezier(.3,.7,.3,1);
}

.cle-05__rise:hover,
.cle-05__rise:focus-visible {
  background-position: 0 100%;
}

.cle-05__wave {
  background-color: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100' preserveAspectRatio='none'%3E%3Cpath d='M0 51 Q11.25 44 22.5 51 T45 51 T67.5 51 T90 51 V101 H0 Z' fill='%230284c7'/%3E%3C/svg%3E");
  background-size: 90px 200%;
  background-position: 0 0;
  transition: background-position .8s cubic-bezier(.34,1.3,.4,1);
}

.cle-05__wave:hover,
.cle-05__wave:focus-visible {
  background-position: 0 100%;
}

.cle-05__flow {
  background-image: repeating-linear-gradient(100deg,oklch(0.58 0.14 230) 0,oklch(0.6 0.19 300) 70px,oklch(0.72 0.17 60) 140px,oklch(0.58 0.14 230) 210px);
  background-size: 210px 100%;
  animation: clh05-flow 2.2s linear infinite paused;
}

.cle-05__flow:hover,
.cle-05__flow:focus-visible {
  animation-play-state: running;
}

@keyframes clh05-flow {
  to {
    background-position: -210px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-05 * {
    transition-duration: .01s !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 Link 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 Fill & Color Wave Hover Effect
Source: https://codefronts.com/motion/css-link-effects/css-text-fill-and-color-wave-hover-effect/

Liquid typography: ink that rises inside the glyphs when you hover. A flat fill that climbs from the baseline, a wavy crest version that actually looks like liquid (an inline SVG wave, no canvas), and a color wave that flows through the letters for as long as the pointer stays. All three are one gradient trick wearing different costumes.
## HTML
```html
<section class="cle-05" aria-label="Text fill and color wave hover demo">
  <div class="cle-05__stage">
    <div class="cle-05__var"><a href="#a" class="cle-05__rise">Reservoir</a><small class="cle-05__cap">A · flat fill rises</small></div>
    <div class="cle-05__var"><a href="#b" class="cle-05__wave">High tide</a><small class="cle-05__cap">B · liquid crest (SVG wave)</small></div>
    <div class="cle-05__var"><a href="#c" class="cle-05__flow">Spectrum</a><small class="cle-05__cap">C · color wave while hovered</small></div>
  </div>
</section>
```
## CSS
```css
.cle-05,
.cle-05 *,
.cle-05 *::before,
.cle-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-05 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.26 0.02 240);
  --brand: oklch(0.58 0.14 230);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-05__stage {
  width: 100%;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 42px;
  border: 1px solid oklch(0.9 0.012 240);
  background: linear-gradient(180deg,oklch(0.985 0.005 220),oklch(0.945 0.015 230));
  padding: 44px 32px;
}

.cle-05__var {
  display: grid;
  gap: 12px;
  justify-items: center;
}

.cle-05__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.03 235);
}

.cle-05 a {
  font-size: clamp(34px,5.6vw,52px);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.1;
  text-decoration: none;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-repeat: repeat-x;
}

.cle-05 a::selection {
  background: oklch(0.85 0.06 230 / .5);
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
}

.cle-05 a:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 6px;
  border-radius: 4px;
}

.cle-05__rise {
  background-image: linear-gradient(to top,var(--brand) 0 50%,var(--ink) 50% 100%);
  background-size: 100% 200%;
  background-position: 0 0;
  transition: background-position .55s cubic-bezier(.3,.7,.3,1);
}

.cle-05__rise:hover,
.cle-05__rise:focus-visible {
  background-position: 0 100%;
}

.cle-05__wave {
  background-color: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 90 100' preserveAspectRatio='none'%3E%3Cpath d='M0 51 Q11.25 44 22.5 51 T45 51 T67.5 51 T90 51 V101 H0 Z' fill='%230284c7'/%3E%3C/svg%3E");
  background-size: 90px 200%;
  background-position: 0 0;
  transition: background-position .8s cubic-bezier(.34,1.3,.4,1);
}

.cle-05__wave:hover,
.cle-05__wave:focus-visible {
  background-position: 0 100%;
}

.cle-05__flow {
  background-image: repeating-linear-gradient(100deg,oklch(0.58 0.14 230) 0,oklch(0.6 0.19 300) 70px,oklch(0.72 0.17 60) 140px,oklch(0.58 0.14 230) 210px);
  background-size: 210px 100%;
  animation: clh05-flow 2.2s linear infinite paused;
}

.cle-05__flow:hover,
.cle-05__flow:focus-visible {
  animation-play-state: running;
}

@keyframes clh05-flow {
  to {
    background-position: -210px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-05 * {
    transition-duration: .01s !important;
    animation: none !important;
  }
}
```

How this works

Clip the background to the glyphs, make the background twice the element's height, and slide the viewing window with background-position:

.rise{
  background-image: linear-gradient(to top, var(--brand) 50%, var(--ink) 50%);
  background-size: 100% 200%;   /* twice as tall as the text */
  background-position: 0 0;      /* window shows the top (ink) half */
  -webkit-background-clip: text; background-clip: text; color: transparent;
  transition: background-position .55s cubic-bezier(.3,.7,.3,1);
}
.rise:hover{ background-position: 0 100%; } /* window slides to the brand half */

Because the image is 200% tall, moving the position from 0 to 100% pans exactly one text-height — the color boundary sweeps up through the letters. Variant B swaps the flat gradient for a repeating SVG data-URI whose top edge is a drawn wave (preserveAspectRatio="none" lets it stretch to the 200% box), so the rising boundary has a liquid crest. C paints a seamless repeating-linear-gradient and scrolls it horizontally in a keyframe loop that idles at animation-play-state: paused and runs on hover — pausing instead of removing the animation is what prevents a snap when the pointer leaves.

Make it yours

  • Fill color: variant A/C tokens are plain CSS vars; B's crest color is hardcoded in the SVG data-URI (fill='%230284c7') — keep it in sync with --brand.
  • Fill speed: the .55s transition; give B a heavier, liquid feel at .8s with the same overshoot curve.
  • Wave frequency in B: the 90px horizontal background-size tile — smaller tile, choppier water.
  • C's flow speed is the 2.2s loop; reverse direction with animation-direction: reverse.

Gotchas — read before shipping

  • background-clip:text needs the -webkit- prefix pair for Safari, and color:transparent (or -webkit-text-fill-color) to reveal the paint.
  • The 200%-height trick assumes a single line: on wrapped text the boundary crosses both lines at once, which reads fine for A/C but breaks B's waterline illusion — keep B on one-line display links.
  • Set an explicit ::selection style; transparent text with default selection colors can vanish when highlighted.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

background-clip:text is Baseline everywhere (prefixed in WebKit); SVG data-URI backgrounds are universal. oklch/color-mix set the stated floor — swap to hex for legacy reach.

Search CodeFronts

Loading…