30 CSS Text Hover Effects02 / 30
CSS Text Fill / Liquid Fill Effect
A headline that fills with water on hover — pure CSS, no canvas, no SVG library. Two stacked copies of the word: the base is a hollow 1px text-stroke, the overlay is gradient-filled via background-clip:text and revealed bottom-up by a clip-path driven by ONE registered @property, so the browser tweens the water level for you. While hovered, a 3s keyframe rocks the clip's wave crest so the surface never sits still. Below it, the classic nav-pill variant fills its background bottom-to-top with nothing but background-size.
Published
The code
<section class="cth-02" aria-label="Liquid text fill on hover demo">
<div class="cth-02__stage">
<div class="cth-02__scene">
<p class="cth-02__eyebrow">Tidewater Swim Co. — Summer '26</p>
<a class="cth-02__word" href="#cth02-top" id="cth02-top">
<span class="cth-02__hollow">TIDEWATER</span>
<span class="cth-02__liquid" aria-hidden="true">TIDEWATER</span>
</a>
<p class="cth-02__sub">Hold the cursor on the wordmark and let it flood. The level is one typed <code>@property</code>; the wave is a 3s keyframe on the clip edge.</p>
<nav class="cth-02__nav" aria-label="Collection">
<a href="#cth02-top">Swimwear</a>
<a href="#cth02-top">Wetsuits</a>
<a href="#cth02-top">Open water</a>
<a href="#cth02-top">Journal</a>
</nav>
<p class="cth-02__hint">Nav pills fill bottom-up with <code>background-size</code> alone — the one-element version of the same idea.</p>
</div>
</div>
</section><section class="cth-02" aria-label="Liquid text fill on hover demo">
<div class="cth-02__stage">
<div class="cth-02__scene">
<p class="cth-02__eyebrow">Tidewater Swim Co. — Summer '26</p>
<a class="cth-02__word" href="#cth02-top" id="cth02-top">
<span class="cth-02__hollow">TIDEWATER</span>
<span class="cth-02__liquid" aria-hidden="true">TIDEWATER</span>
</a>
<p class="cth-02__sub">Hold the cursor on the wordmark and let it flood. The level is one typed <code>@property</code>; the wave is a 3s keyframe on the clip edge.</p>
<nav class="cth-02__nav" aria-label="Collection">
<a href="#cth02-top">Swimwear</a>
<a href="#cth02-top">Wetsuits</a>
<a href="#cth02-top">Open water</a>
<a href="#cth02-top">Journal</a>
</nav>
<p class="cth-02__hint">Nav pills fill bottom-up with <code>background-size</code> alone — the one-element version of the same idea.</p>
</div>
</div>
</section>@property --cth02-fill {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
@property --cth02-bob {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
.cth-02,
.cth-02 *,
.cth-02 *::before,
.cth-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cth-02 {
--bg: oklch(0.17 0.03 250);
--ink: oklch(0.95 0.01 230);
--mut: oklch(0.66 0.03 240);
--water-hi: oklch(0.85 0.11 220);
--water-lo: oklch(0.55 0.19 255);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
}
.cth-02__stage {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
background: radial-gradient(120% 90% at 50% 0%,oklch(0.22 0.045 255),var(--bg) 60%);
padding: clamp(20px,4vw,56px);
container: cth02/inline-size;
}
.cth-02__scene {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: clamp(18px,3cqi,26px);
width: min(100%,820px);
}
.cth-02__eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--mut);
}
.cth-02__word {
--cth02-fill: 0%;
position: relative;
display: inline-block;
text-decoration: none;
font-size: clamp(48px,12cqi,124px);
font-weight: 900;
letter-spacing: -.03em;
line-height: 1.04;
padding: .06em .04em;
transition: --cth02-fill .9s cubic-bezier(.3,.9,.2,1);
}
.cth-02__word:hover,
.cth-02__word:focus-visible {
--cth02-fill: 104%;
}
.cth-02__word:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 8px;
border-radius: 4px;
}
.cth-02__hollow {
display: block;
color: transparent;
-webkit-text-stroke: 1.5px color-mix(in oklch,var(--ink) 55%,transparent);
}
.cth-02__liquid {
position: absolute;
inset: 0;
padding: .06em .04em;
display: block;
background: linear-gradient(180deg,var(--water-hi) 0%,var(--water-hi) 6%,var(--water-lo) 60%,oklch(0.4 0.16 262) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
clip-path: inset(calc(100% - var(--cth02-fill) + var(--cth02-bob)) -6% -6% -6%);
}
.cth-02__word:hover .cth-02__liquid,
.cth-02__word:focus-visible .cth-02__liquid {
animation: cth02-bob 3s ease-in-out .9s infinite;
}
@keyframes cth02-bob {
0%,
100% {
--cth02-bob: 0%;
}
50% {
--cth02-bob: 1.6%;
}
}
.cth-02__sub {
font-size: 14.5px;
line-height: 1.7;
color: var(--mut);
max-width: 52ch;
}
.cth-02__sub code,
.cth-02__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .9em;
background: oklch(0.26 0.04 250);
padding: 1px 6px;
border-radius: 5px;
color: var(--water-hi);
}
.cth-02__nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.cth-02__nav a {
display: grid;
place-items: center;
min-height: 44px;
padding: 0 22px;
border: 1.5px solid color-mix(in oklch,var(--ink) 30%,transparent);
border-radius: 99px;
color: var(--ink);
text-decoration: none;
font-size: 14px;
font-weight: 650;
background-image: linear-gradient(180deg,var(--water-hi),var(--water-lo));
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0%;
transition: background-size .45s cubic-bezier(.3,.9,.2,1),color .3s,border-color .3s;
}
.cth-02__nav a:hover,
.cth-02__nav a:focus-visible {
background-size: 100% 100%;
color: oklch(0.15 0.03 255);
border-color: transparent;
}
.cth-02__nav a:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 3px;
}
.cth-02__hint {
font-size: 12.5px;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.cth-02 * {
transition-duration: .01ms !important;
animation: none !important;
}
}@property --cth02-fill {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
@property --cth02-bob {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
.cth-02,
.cth-02 *,
.cth-02 *::before,
.cth-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cth-02 {
--bg: oklch(0.17 0.03 250);
--ink: oklch(0.95 0.01 230);
--mut: oklch(0.66 0.03 240);
--water-hi: oklch(0.85 0.11 220);
--water-lo: oklch(0.55 0.19 255);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
}
.cth-02__stage {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
background: radial-gradient(120% 90% at 50% 0%,oklch(0.22 0.045 255),var(--bg) 60%);
padding: clamp(20px,4vw,56px);
container: cth02/inline-size;
}
.cth-02__scene {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: clamp(18px,3cqi,26px);
width: min(100%,820px);
}
.cth-02__eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--mut);
}
.cth-02__word {
--cth02-fill: 0%;
position: relative;
display: inline-block;
text-decoration: none;
font-size: clamp(48px,12cqi,124px);
font-weight: 900;
letter-spacing: -.03em;
line-height: 1.04;
padding: .06em .04em;
transition: --cth02-fill .9s cubic-bezier(.3,.9,.2,1);
}
.cth-02__word:hover,
.cth-02__word:focus-visible {
--cth02-fill: 104%;
}
.cth-02__word:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 8px;
border-radius: 4px;
}
.cth-02__hollow {
display: block;
color: transparent;
-webkit-text-stroke: 1.5px color-mix(in oklch,var(--ink) 55%,transparent);
}
.cth-02__liquid {
position: absolute;
inset: 0;
padding: .06em .04em;
display: block;
background: linear-gradient(180deg,var(--water-hi) 0%,var(--water-hi) 6%,var(--water-lo) 60%,oklch(0.4 0.16 262) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
clip-path: inset(calc(100% - var(--cth02-fill) + var(--cth02-bob)) -6% -6% -6%);
}
.cth-02__word:hover .cth-02__liquid,
.cth-02__word:focus-visible .cth-02__liquid {
animation: cth02-bob 3s ease-in-out .9s infinite;
}
@keyframes cth02-bob {
0%,
100% {
--cth02-bob: 0%;
}
50% {
--cth02-bob: 1.6%;
}
}
.cth-02__sub {
font-size: 14.5px;
line-height: 1.7;
color: var(--mut);
max-width: 52ch;
}
.cth-02__sub code,
.cth-02__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .9em;
background: oklch(0.26 0.04 250);
padding: 1px 6px;
border-radius: 5px;
color: var(--water-hi);
}
.cth-02__nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.cth-02__nav a {
display: grid;
place-items: center;
min-height: 44px;
padding: 0 22px;
border: 1.5px solid color-mix(in oklch,var(--ink) 30%,transparent);
border-radius: 99px;
color: var(--ink);
text-decoration: none;
font-size: 14px;
font-weight: 650;
background-image: linear-gradient(180deg,var(--water-hi),var(--water-lo));
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0%;
transition: background-size .45s cubic-bezier(.3,.9,.2,1),color .3s,border-color .3s;
}
.cth-02__nav a:hover,
.cth-02__nav a:focus-visible {
background-size: 100% 100%;
color: oklch(0.15 0.03 255);
border-color: transparent;
}
.cth-02__nav a:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 3px;
}
.cth-02__hint {
font-size: 12.5px;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.cth-02 * {
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 Fill / Liquid Fill Effect
Source: https://codefronts.com/motion/css-text-hover-effects/css-text-fill-liquid-fill-effect/
A headline that fills with water on hover — pure CSS, no canvas, no SVG library. Two stacked copies of the word: the base is a hollow 1px text-stroke, the overlay is gradient-filled via background-clip:text and revealed bottom-up by a clip-path driven by ONE registered @property, so the browser tweens the water level for you. While hovered, a 3s keyframe rocks the clip's wave crest so the surface never sits still. Below it, the classic nav-pill variant fills its background bottom-to-top with nothing but background-size.
## HTML
```html
<section class="cth-02" aria-label="Liquid text fill on hover demo">
<div class="cth-02__stage">
<div class="cth-02__scene">
<p class="cth-02__eyebrow">Tidewater Swim Co. — Summer '26</p>
<a class="cth-02__word" href="#cth02-top" id="cth02-top">
<span class="cth-02__hollow">TIDEWATER</span>
<span class="cth-02__liquid" aria-hidden="true">TIDEWATER</span>
</a>
<p class="cth-02__sub">Hold the cursor on the wordmark and let it flood. The level is one typed <code>@property</code>; the wave is a 3s keyframe on the clip edge.</p>
<nav class="cth-02__nav" aria-label="Collection">
<a href="#cth02-top">Swimwear</a>
<a href="#cth02-top">Wetsuits</a>
<a href="#cth02-top">Open water</a>
<a href="#cth02-top">Journal</a>
</nav>
<p class="cth-02__hint">Nav pills fill bottom-up with <code>background-size</code> alone — the one-element version of the same idea.</p>
</div>
</div>
</section>
```
## CSS
```css
@property --cth02-fill {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
@property --cth02-bob {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
.cth-02,
.cth-02 *,
.cth-02 *::before,
.cth-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cth-02 {
--bg: oklch(0.17 0.03 250);
--ink: oklch(0.95 0.01 230);
--mut: oklch(0.66 0.03 240);
--water-hi: oklch(0.85 0.11 220);
--water-lo: oklch(0.55 0.19 255);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
}
.cth-02__stage {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
background: radial-gradient(120% 90% at 50% 0%,oklch(0.22 0.045 255),var(--bg) 60%);
padding: clamp(20px,4vw,56px);
container: cth02/inline-size;
}
.cth-02__scene {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: clamp(18px,3cqi,26px);
width: min(100%,820px);
}
.cth-02__eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--mut);
}
.cth-02__word {
--cth02-fill: 0%;
position: relative;
display: inline-block;
text-decoration: none;
font-size: clamp(48px,12cqi,124px);
font-weight: 900;
letter-spacing: -.03em;
line-height: 1.04;
padding: .06em .04em;
transition: --cth02-fill .9s cubic-bezier(.3,.9,.2,1);
}
.cth-02__word:hover,
.cth-02__word:focus-visible {
--cth02-fill: 104%;
}
.cth-02__word:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 8px;
border-radius: 4px;
}
.cth-02__hollow {
display: block;
color: transparent;
-webkit-text-stroke: 1.5px color-mix(in oklch,var(--ink) 55%,transparent);
}
.cth-02__liquid {
position: absolute;
inset: 0;
padding: .06em .04em;
display: block;
background: linear-gradient(180deg,var(--water-hi) 0%,var(--water-hi) 6%,var(--water-lo) 60%,oklch(0.4 0.16 262) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
clip-path: inset(calc(100% - var(--cth02-fill) + var(--cth02-bob)) -6% -6% -6%);
}
.cth-02__word:hover .cth-02__liquid,
.cth-02__word:focus-visible .cth-02__liquid {
animation: cth02-bob 3s ease-in-out .9s infinite;
}
@keyframes cth02-bob {
0%,
100% {
--cth02-bob: 0%;
}
50% {
--cth02-bob: 1.6%;
}
}
.cth-02__sub {
font-size: 14.5px;
line-height: 1.7;
color: var(--mut);
max-width: 52ch;
}
.cth-02__sub code,
.cth-02__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .9em;
background: oklch(0.26 0.04 250);
padding: 1px 6px;
border-radius: 5px;
color: var(--water-hi);
}
.cth-02__nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.cth-02__nav a {
display: grid;
place-items: center;
min-height: 44px;
padding: 0 22px;
border: 1.5px solid color-mix(in oklch,var(--ink) 30%,transparent);
border-radius: 99px;
color: var(--ink);
text-decoration: none;
font-size: 14px;
font-weight: 650;
background-image: linear-gradient(180deg,var(--water-hi),var(--water-lo));
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0%;
transition: background-size .45s cubic-bezier(.3,.9,.2,1),color .3s,border-color .3s;
}
.cth-02__nav a:hover,
.cth-02__nav a:focus-visible {
background-size: 100% 100%;
color: oklch(0.15 0.03 255);
border-color: transparent;
}
.cth-02__nav a:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 3px;
}
.cth-02__hint {
font-size: 12.5px;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.cth-02 * {
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 Fill / Liquid Fill Effect
Source: https://codefronts.com/motion/css-text-hover-effects/css-text-fill-liquid-fill-effect/
A headline that fills with water on hover — pure CSS, no canvas, no SVG library. Two stacked copies of the word: the base is a hollow 1px text-stroke, the overlay is gradient-filled via background-clip:text and revealed bottom-up by a clip-path driven by ONE registered @property, so the browser tweens the water level for you. While hovered, a 3s keyframe rocks the clip's wave crest so the surface never sits still. Below it, the classic nav-pill variant fills its background bottom-to-top with nothing but background-size.
## HTML
```html
<section class="cth-02" aria-label="Liquid text fill on hover demo">
<div class="cth-02__stage">
<div class="cth-02__scene">
<p class="cth-02__eyebrow">Tidewater Swim Co. — Summer '26</p>
<a class="cth-02__word" href="#cth02-top" id="cth02-top">
<span class="cth-02__hollow">TIDEWATER</span>
<span class="cth-02__liquid" aria-hidden="true">TIDEWATER</span>
</a>
<p class="cth-02__sub">Hold the cursor on the wordmark and let it flood. The level is one typed <code>@property</code>; the wave is a 3s keyframe on the clip edge.</p>
<nav class="cth-02__nav" aria-label="Collection">
<a href="#cth02-top">Swimwear</a>
<a href="#cth02-top">Wetsuits</a>
<a href="#cth02-top">Open water</a>
<a href="#cth02-top">Journal</a>
</nav>
<p class="cth-02__hint">Nav pills fill bottom-up with <code>background-size</code> alone — the one-element version of the same idea.</p>
</div>
</div>
</section>
```
## CSS
```css
@property --cth02-fill {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
@property --cth02-bob {
syntax: '<percentage>';
inherits: true;
initial-value: 0%;
}
.cth-02,
.cth-02 *,
.cth-02 *::before,
.cth-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cth-02 {
--bg: oklch(0.17 0.03 250);
--ink: oklch(0.95 0.01 230);
--mut: oklch(0.66 0.03 240);
--water-hi: oklch(0.85 0.11 220);
--water-lo: oklch(0.55 0.19 255);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
}
.cth-02__stage {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
background: radial-gradient(120% 90% at 50% 0%,oklch(0.22 0.045 255),var(--bg) 60%);
padding: clamp(20px,4vw,56px);
container: cth02/inline-size;
}
.cth-02__scene {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: clamp(18px,3cqi,26px);
width: min(100%,820px);
}
.cth-02__eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--mut);
}
.cth-02__word {
--cth02-fill: 0%;
position: relative;
display: inline-block;
text-decoration: none;
font-size: clamp(48px,12cqi,124px);
font-weight: 900;
letter-spacing: -.03em;
line-height: 1.04;
padding: .06em .04em;
transition: --cth02-fill .9s cubic-bezier(.3,.9,.2,1);
}
.cth-02__word:hover,
.cth-02__word:focus-visible {
--cth02-fill: 104%;
}
.cth-02__word:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 8px;
border-radius: 4px;
}
.cth-02__hollow {
display: block;
color: transparent;
-webkit-text-stroke: 1.5px color-mix(in oklch,var(--ink) 55%,transparent);
}
.cth-02__liquid {
position: absolute;
inset: 0;
padding: .06em .04em;
display: block;
background: linear-gradient(180deg,var(--water-hi) 0%,var(--water-hi) 6%,var(--water-lo) 60%,oklch(0.4 0.16 262) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
clip-path: inset(calc(100% - var(--cth02-fill) + var(--cth02-bob)) -6% -6% -6%);
}
.cth-02__word:hover .cth-02__liquid,
.cth-02__word:focus-visible .cth-02__liquid {
animation: cth02-bob 3s ease-in-out .9s infinite;
}
@keyframes cth02-bob {
0%,
100% {
--cth02-bob: 0%;
}
50% {
--cth02-bob: 1.6%;
}
}
.cth-02__sub {
font-size: 14.5px;
line-height: 1.7;
color: var(--mut);
max-width: 52ch;
}
.cth-02__sub code,
.cth-02__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .9em;
background: oklch(0.26 0.04 250);
padding: 1px 6px;
border-radius: 5px;
color: var(--water-hi);
}
.cth-02__nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.cth-02__nav a {
display: grid;
place-items: center;
min-height: 44px;
padding: 0 22px;
border: 1.5px solid color-mix(in oklch,var(--ink) 30%,transparent);
border-radius: 99px;
color: var(--ink);
text-decoration: none;
font-size: 14px;
font-weight: 650;
background-image: linear-gradient(180deg,var(--water-hi),var(--water-lo));
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0%;
transition: background-size .45s cubic-bezier(.3,.9,.2,1),color .3s,border-color .3s;
}
.cth-02__nav a:hover,
.cth-02__nav a:focus-visible {
background-size: 100% 100%;
color: oklch(0.15 0.03 255);
border-color: transparent;
}
.cth-02__nav a:focus-visible {
outline: 2px solid var(--water-hi);
outline-offset: 3px;
}
.cth-02__hint {
font-size: 12.5px;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.cth-02 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```How this works
The liquid is a custom property with a type. Registering --fill as a real <percentage> makes it transitionable — so the clip-path that consumes it glides instead of snapping:
@property --cth02-fill{
syntax:'<percentage>'; inherits:true; initial-value:0%; }
.word{ --cth02-fill:0%; transition:--cth02-fill .9s cubic-bezier(.3,.9,.2,1); }
.word:hover,.word:focus-visible{ --cth02-fill:104%; }
.word .liquid{ /* the filled copy on top */
position:absolute; inset:0;
background:linear-gradient(180deg,#7de2ff,#0f6bff);
-webkit-background-clip:text; background-clip:text;
color:transparent;
clip-path:inset(calc(100% - var(--cth02-fill)) -5% -5% -5%); }Read the clip inside-out: at --fill:0% the inset's top edge is 100% — the overlay is clipped to nothing and only the hollow stroke copy underneath shows. Hover transitions the property, the calc() re-evaluates every frame, and the top edge of the clip window sinks from 100% to below zero: water rising through the glyphs. The overlay is aria-hidden and the base copy keeps the real text, so assistive tech reads one word, not two.
Why clip-path instead of animating background-position on one copy? Because the two-layer build gives you a different treatment per layer — hollow stroke below, liquid above, plus the wave: a keyframe nudging the clip's top edge ±1.5% runs only while hovered, and a highlight streak in the gradient reads as the meniscus. The nav pills underneath are the one-layer economy version: background-size:100% 0% → 100% 100% anchored at bottom — same rising metaphor, one box, zero pseudo-elements.
Make it yours
- Liquid color is one gradient: swap the blues for
linear-gradient(180deg,oklch(0.85 0.16 85),oklch(0.65 0.2 55))and it pours honey; near-black on paper-white reads as ink. - Fill direction:
inset(-5% -5% calc(100% - var(--fill)) -5%)pours top-down; left-to-right (inset(-5% calc(100% - var(--fill)) -5% -5%)) reads as a progress wipe. - Overshoot the level: transitioning to 104% instead of 100% lets the wave crest bob without exposing the stroke copy at the top of tall glyphs.
- Slower is heavier: 900ms feels like water; 400ms feels like mercury; 2s with a linear ease feels like a tank filling — pick the physics that matches the brand.
Gotchas — read before shipping
- background-clip:text still needs the -webkit- prefix in every Chromium and WebKit build — write both, prefixed first, or the gradient paints as a solid box behind the glyphs.
- Without @property registration the custom property is a string: the transition declaration is ignored and the fill SNAPS. Firefox shipped @property in 128 (July 2024) — earlier Firefox gets the snap, still legible.
- Negative inset edges (-5%) are load-bearing: stroke width and wave bob extend slightly outside the em box, and a 0 edge clips them into a hard line.
- Set color:transparent (not opacity:0) on the clipped copy — opacity would also fade the gradient you're trying to show.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
clip-path inset(), background-clip:text and the two-layer build are years-universal; the smooth tween needs @property (Chrome 85+/Safari 16.4+/Firefox 128+). Below that the level jumps between 0% and full — functional, unanimated.