29 / 30
All 30 in 30 Tailwind CSS Text Hover Effects
- 01Tailwind CSS Animated Underline Hover Effect
- 02Tailwind Liquid Text Fill Hover Animation
- 03Tailwind Gradient Text Hover Effect with bg-clip-text
- 04Tailwind Animated Strikethrough Text Hover Effect
- 05Tailwind Text Swap and Flip Up Hover Effect
- 06Tailwind CSS Glitch Text Hover Effect
- 07Tailwind Neon Glow Text with Flicker Animation
- 08Tailwind Metallic Gold Shimmer Text Effect
- 09Tailwind 3D Text Pop and Lift Hover Effect
- 10Tailwind Staggered Letter Wave Hover Animation
- 11Tailwind Spotlight Text Reveal on Mouse Hover
- 12Tailwind Hover to Reveal Hidden Spoiler Text
- 13Tailwind Blur to Focus Text Hover Effect
- 14Tailwind Magnetic Hover Effect for Buttons and Text
- 15Tailwind Letter Spacing Expand on Hover (Tracking Animation)
- 16Tailwind Outline Text Stroke to Fill Hover Effect
- 17Tailwind Text Scramble and Matrix Decrypt Hover Effect
- 18Tailwind Animated Border Button Hover Effect
- 19Tailwind Elastic Rubber Band Text Hover Animation
- 20Tailwind Variable Font Weight Hover Animation
- 21Tailwind Animated Rainbow Gradient Text on Hover
- 22Tailwind 3D Perspective Tilt Text Hover Effect
- 23Tailwind Smoke Dissolve Text Hover Effect
- 24Tailwind Fire Text Effect with Heat Wave Glow
- 25Tailwind Dynamic Text Shadow that Follows the Cursor
- 26Tailwind Infinite Text Marquee that Speeds Up on Hover
- 27Tailwind Duotone Split Color Text Hover Effect
- 28Tailwind CSS Tooltip on Hover (No JavaScript)
- 29Tailwind SVG Water Ripple Distortion Text Effect
- 30Tailwind Clip-Path Text Reveal Hover Animation
Tailwind SVG Water Ripple Distortion Text Effect
A surf-report wordmark that slips underwater on hover: a hidden SVG filter (feTurbulence + feDisplacementMap, with an animated baseFrequency) distorts an aqua clone, applied with [filter:url(#tth29-ripple)]. Because filter:url() can't transition, the crisp word and the rippled clone crossfade instead.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tth-29 w-full min-h-svh grid place-items-center bg-tth29-bg bg-[radial-gradient(100%_85%_at_50%_100%,var(--color-tth29-deep),var(--color-tth29-bg)_70%)] px-5 py-14 font-tth29-sans text-tth29-ink" aria-label="Ripple distortion text demo">
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><filter id="tth29-ripple" x="-25%" y="-25%" width="150%" height="150%"><feTurbulence type="turbulence" baseFrequency="0.012 0.05" numOctaves="2" seed="4" result="n"><animate attributeName="baseFrequency" dur="7s" values="0.012 0.05; 0.017 0.09; 0.012 0.05" repeatCount="indefinite"/></feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="16" xChannelSelector="R" yChannelSelector="G"/></filter></svg>
<div class="flex flex-col items-center gap-[clamp(16px,3vw,26px)] text-center">
<p class="text-xs font-bold uppercase tracking-[0.26em] text-tth29-mut">Undertow — surf & swell reports, delivered at dawn</p>
<a id="tth29-top" href="#tth29-top" class="group/word relative inline-block rounded-md px-[0.12em] py-[0.15em] font-tth29-display text-[clamp(64px,14vw,150px)] font-black uppercase leading-none tracking-[0.01em] no-underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-tth29-acc">
<span class="block opacity-100 transition-[opacity,color] duration-600 group-hover/word:opacity-6 group-focus-visible/word:opacity-6 motion-reduce:transition-none motion-reduce:group-hover/word:opacity-100 motion-reduce:group-hover/word:text-tth29-acc">UNDERTOW</span>
<span aria-hidden="true" class="absolute inset-x-[0.12em] inset-y-[0.15em] block text-tth29-acc opacity-0 [filter:url(#tth29-ripple)] transition-opacity duration-600 group-hover/word:opacity-100 group-focus-visible/word:opacity-100 motion-reduce:hidden">UNDERTOW</span>
</a>
<svg viewBox="0 0 220 14" aria-hidden="true" class="w-[min(220px,60%)] text-tth29-acc/55"><path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" d="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"><animate attributeName="d" dur="2.6s" repeatCount="indefinite" values="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 13, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"/></path></svg>
<form class="flex w-full max-w-md flex-wrap justify-center gap-2" onsubmit="return false" aria-label="Subscribe to dawn reports">
<label for="tth29-email" class="sr-only">Email address</label>
<input id="tth29-email" type="email" placeholder="you@lineup.surf" class="min-h-12 min-w-0 flex-1 rounded-full border border-tth29-ink/20 bg-white/5 px-5 text-[15px] text-tth29-ink placeholder:text-tth29-mut focus:border-tth29-acc focus:outline-none focus-visible:ring-2 focus-visible:ring-tth29-acc/40">
<button type="submit" class="min-h-12 cursor-pointer rounded-full bg-tth29-acc px-6 text-[15px] font-bold text-tth29-bg transition-[translate,box-shadow] duration-300 hover:-translate-y-0.5 hover:shadow-[0_10px_30px_-8px_var(--color-tth29-acc)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth29-acc">Get the dawn report</button>
</form>
<p class="max-w-[58ch] text-[12.5px] leading-[1.7] text-tth29-mut">Hover the wordmark — it slips underwater. <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feTurbulence</code> + <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feDisplacementMap</code> do the physics; a crossfaded clone hides that <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">filter:url()</code> can't transition.</p>
</div>
</section><section class="tth-29 w-full min-h-svh grid place-items-center bg-tth29-bg bg-[radial-gradient(100%_85%_at_50%_100%,var(--color-tth29-deep),var(--color-tth29-bg)_70%)] px-5 py-14 font-tth29-sans text-tth29-ink" aria-label="Ripple distortion text demo">
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><filter id="tth29-ripple" x="-25%" y="-25%" width="150%" height="150%"><feTurbulence type="turbulence" baseFrequency="0.012 0.05" numOctaves="2" seed="4" result="n"><animate attributeName="baseFrequency" dur="7s" values="0.012 0.05; 0.017 0.09; 0.012 0.05" repeatCount="indefinite"/></feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="16" xChannelSelector="R" yChannelSelector="G"/></filter></svg>
<div class="flex flex-col items-center gap-[clamp(16px,3vw,26px)] text-center">
<p class="text-xs font-bold uppercase tracking-[0.26em] text-tth29-mut">Undertow — surf & swell reports, delivered at dawn</p>
<a id="tth29-top" href="#tth29-top" class="group/word relative inline-block rounded-md px-[0.12em] py-[0.15em] font-tth29-display text-[clamp(64px,14vw,150px)] font-black uppercase leading-none tracking-[0.01em] no-underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-tth29-acc">
<span class="block opacity-100 transition-[opacity,color] duration-600 group-hover/word:opacity-6 group-focus-visible/word:opacity-6 motion-reduce:transition-none motion-reduce:group-hover/word:opacity-100 motion-reduce:group-hover/word:text-tth29-acc">UNDERTOW</span>
<span aria-hidden="true" class="absolute inset-x-[0.12em] inset-y-[0.15em] block text-tth29-acc opacity-0 [filter:url(#tth29-ripple)] transition-opacity duration-600 group-hover/word:opacity-100 group-focus-visible/word:opacity-100 motion-reduce:hidden">UNDERTOW</span>
</a>
<svg viewBox="0 0 220 14" aria-hidden="true" class="w-[min(220px,60%)] text-tth29-acc/55"><path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" d="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"><animate attributeName="d" dur="2.6s" repeatCount="indefinite" values="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 13, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"/></path></svg>
<form class="flex w-full max-w-md flex-wrap justify-center gap-2" onsubmit="return false" aria-label="Subscribe to dawn reports">
<label for="tth29-email" class="sr-only">Email address</label>
<input id="tth29-email" type="email" placeholder="you@lineup.surf" class="min-h-12 min-w-0 flex-1 rounded-full border border-tth29-ink/20 bg-white/5 px-5 text-[15px] text-tth29-ink placeholder:text-tth29-mut focus:border-tth29-acc focus:outline-none focus-visible:ring-2 focus-visible:ring-tth29-acc/40">
<button type="submit" class="min-h-12 cursor-pointer rounded-full bg-tth29-acc px-6 text-[15px] font-bold text-tth29-bg transition-[translate,box-shadow] duration-300 hover:-translate-y-0.5 hover:shadow-[0_10px_30px_-8px_var(--color-tth29-acc)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth29-acc">Get the dawn report</button>
</form>
<p class="max-w-[58ch] text-[12.5px] leading-[1.7] text-tth29-mut">Hover the wordmark — it slips underwater. <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feTurbulence</code> + <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feDisplacementMap</code> do the physics; a crossfaded clone hides that <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">filter:url()</code> can't transition.</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700..900&family=Outfit:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700..900&family=Outfit:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
filter:url(#cth29-ripple)became the arbitrary property[filter:url(#tth29-ripple)]; the SVG filter id is namespaced because ids are document-global.- The reduced-motion media block became
motion-reduce:hiddenon the clone andmotion-reduce:group-hover/word:text-tth29-accon the crisp word. - UI upgrade from the CSS original: a working subscribe form (labelled input + 48px button) gives the surf brand an actual conversion point under the hero.
- UI upgrade: Big Shoulders Display's tall condensed caps show far more ripple per letter, and muted copy rose from
0.6to0.71L (6.6:1).
Here's a working CSS demo 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: Tailwind SVG Water Ripple Distortion Text Effect
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-svg-water-ripple-distortion-text-effect/
A surf-report wordmark that slips underwater on hover: a hidden SVG filter (feTurbulence + feDisplacementMap, with an animated baseFrequency) distorts an aqua clone, applied with [filter:url(#tth29-ripple)]. Because filter:url() can't transition, the crisp word and the rippled clone crossfade instead.
## HTML
```html
<section class="tth-29 w-full min-h-svh grid place-items-center bg-tth29-bg bg-[radial-gradient(100%_85%_at_50%_100%,var(--color-tth29-deep),var(--color-tth29-bg)_70%)] px-5 py-14 font-tth29-sans text-tth29-ink" aria-label="Ripple distortion text demo">
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><filter id="tth29-ripple" x="-25%" y="-25%" width="150%" height="150%"><feTurbulence type="turbulence" baseFrequency="0.012 0.05" numOctaves="2" seed="4" result="n"><animate attributeName="baseFrequency" dur="7s" values="0.012 0.05; 0.017 0.09; 0.012 0.05" repeatCount="indefinite"/></feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="16" xChannelSelector="R" yChannelSelector="G"/></filter></svg>
<div class="flex flex-col items-center gap-[clamp(16px,3vw,26px)] text-center">
<p class="text-xs font-bold uppercase tracking-[0.26em] text-tth29-mut">Undertow — surf & swell reports, delivered at dawn</p>
<a id="tth29-top" href="#tth29-top" class="group/word relative inline-block rounded-md px-[0.12em] py-[0.15em] font-tth29-display text-[clamp(64px,14vw,150px)] font-black uppercase leading-none tracking-[0.01em] no-underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-tth29-acc">
<span class="block opacity-100 transition-[opacity,color] duration-600 group-hover/word:opacity-6 group-focus-visible/word:opacity-6 motion-reduce:transition-none motion-reduce:group-hover/word:opacity-100 motion-reduce:group-hover/word:text-tth29-acc">UNDERTOW</span>
<span aria-hidden="true" class="absolute inset-x-[0.12em] inset-y-[0.15em] block text-tth29-acc opacity-0 [filter:url(#tth29-ripple)] transition-opacity duration-600 group-hover/word:opacity-100 group-focus-visible/word:opacity-100 motion-reduce:hidden">UNDERTOW</span>
</a>
<svg viewBox="0 0 220 14" aria-hidden="true" class="w-[min(220px,60%)] text-tth29-acc/55"><path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" d="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"><animate attributeName="d" dur="2.6s" repeatCount="indefinite" values="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 13, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"/></path></svg>
<form class="flex w-full max-w-md flex-wrap justify-center gap-2" onsubmit="return false" aria-label="Subscribe to dawn reports">
<label for="tth29-email" class="sr-only">Email address</label>
<input id="tth29-email" type="email" placeholder="you@lineup.surf" class="min-h-12 min-w-0 flex-1 rounded-full border border-tth29-ink/20 bg-white/5 px-5 text-[15px] text-tth29-ink placeholder:text-tth29-mut focus:border-tth29-acc focus:outline-none focus-visible:ring-2 focus-visible:ring-tth29-acc/40">
<button type="submit" class="min-h-12 cursor-pointer rounded-full bg-tth29-acc px-6 text-[15px] font-bold text-tth29-bg transition-[translate,box-shadow] duration-300 hover:-translate-y-0.5 hover:shadow-[0_10px_30px_-8px_var(--color-tth29-acc)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth29-acc">Get the dawn report</button>
</form>
<p class="max-w-[58ch] text-[12.5px] leading-[1.7] text-tth29-mut">Hover the wordmark — it slips underwater. <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feTurbulence</code> + <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feDisplacementMap</code> do the physics; a crossfaded clone hides that <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">filter:url()</code> can't transition.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}
```Here's a working CSS demo 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: Tailwind SVG Water Ripple Distortion Text Effect
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-svg-water-ripple-distortion-text-effect/
A surf-report wordmark that slips underwater on hover: a hidden SVG filter (feTurbulence + feDisplacementMap, with an animated baseFrequency) distorts an aqua clone, applied with [filter:url(#tth29-ripple)]. Because filter:url() can't transition, the crisp word and the rippled clone crossfade instead.
## HTML
```html
<section class="tth-29 w-full min-h-svh grid place-items-center bg-tth29-bg bg-[radial-gradient(100%_85%_at_50%_100%,var(--color-tth29-deep),var(--color-tth29-bg)_70%)] px-5 py-14 font-tth29-sans text-tth29-ink" aria-label="Ripple distortion text demo">
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><filter id="tth29-ripple" x="-25%" y="-25%" width="150%" height="150%"><feTurbulence type="turbulence" baseFrequency="0.012 0.05" numOctaves="2" seed="4" result="n"><animate attributeName="baseFrequency" dur="7s" values="0.012 0.05; 0.017 0.09; 0.012 0.05" repeatCount="indefinite"/></feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="16" xChannelSelector="R" yChannelSelector="G"/></filter></svg>
<div class="flex flex-col items-center gap-[clamp(16px,3vw,26px)] text-center">
<p class="text-xs font-bold uppercase tracking-[0.26em] text-tth29-mut">Undertow — surf & swell reports, delivered at dawn</p>
<a id="tth29-top" href="#tth29-top" class="group/word relative inline-block rounded-md px-[0.12em] py-[0.15em] font-tth29-display text-[clamp(64px,14vw,150px)] font-black uppercase leading-none tracking-[0.01em] no-underline focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-tth29-acc">
<span class="block opacity-100 transition-[opacity,color] duration-600 group-hover/word:opacity-6 group-focus-visible/word:opacity-6 motion-reduce:transition-none motion-reduce:group-hover/word:opacity-100 motion-reduce:group-hover/word:text-tth29-acc">UNDERTOW</span>
<span aria-hidden="true" class="absolute inset-x-[0.12em] inset-y-[0.15em] block text-tth29-acc opacity-0 [filter:url(#tth29-ripple)] transition-opacity duration-600 group-hover/word:opacity-100 group-focus-visible/word:opacity-100 motion-reduce:hidden">UNDERTOW</span>
</a>
<svg viewBox="0 0 220 14" aria-hidden="true" class="w-[min(220px,60%)] text-tth29-acc/55"><path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" d="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"><animate attributeName="d" dur="2.6s" repeatCount="indefinite" values="M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 13, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7; M2 7 Q 15 1, 28 7 T 54 7 T 80 7 T 106 7 T 132 7 T 158 7 T 184 7 T 210 7"/></path></svg>
<form class="flex w-full max-w-md flex-wrap justify-center gap-2" onsubmit="return false" aria-label="Subscribe to dawn reports">
<label for="tth29-email" class="sr-only">Email address</label>
<input id="tth29-email" type="email" placeholder="you@lineup.surf" class="min-h-12 min-w-0 flex-1 rounded-full border border-tth29-ink/20 bg-white/5 px-5 text-[15px] text-tth29-ink placeholder:text-tth29-mut focus:border-tth29-acc focus:outline-none focus-visible:ring-2 focus-visible:ring-tth29-acc/40">
<button type="submit" class="min-h-12 cursor-pointer rounded-full bg-tth29-acc px-6 text-[15px] font-bold text-tth29-bg transition-[translate,box-shadow] duration-300 hover:-translate-y-0.5 hover:shadow-[0_10px_30px_-8px_var(--color-tth29-acc)] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth29-acc">Get the dawn report</button>
</form>
<p class="max-w-[58ch] text-[12.5px] leading-[1.7] text-tth29-mut">Hover the wordmark — it slips underwater. <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feTurbulence</code> + <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">feDisplacementMap</code> do the physics; a crossfaded clone hides that <code class="rounded bg-tth29-chip px-1.5 py-px font-tth29-mono text-[0.9em] text-tth29-acc">filter:url()</code> can't transition.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth29-bg: #0a1a22;
--color-tth29-bg: oklch(0.17 0.025 230);
--color-tth29-deep: #0f2e3d;
--color-tth29-deep: oklch(0.24 0.05 220);
--color-tth29-chip: #13252f;
--color-tth29-chip: oklch(0.24 0.03 225);
--color-tth29-ink: #e3f2f6;
--color-tth29-ink: oklch(0.94 0.02 210);
--color-tth29-mut: #8fa9b5;
--color-tth29-mut: oklch(0.71 0.04 220);
--color-tth29-acc: #5fd6e0;
--color-tth29-acc: oklch(0.8 0.12 195);
--font-tth29-display: 'Big Shoulders Display', Impact, sans-serif;
--font-tth29-sans: 'Outfit', system-ui, sans-serif;
--font-tth29-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-29 {
width: 100%;
min-height: 100svh;
}
```How this works
The physics. feTurbulence generates Perlin noise; feDisplacementMap shifts every pixel of the text by an amount read from that noise's red and green channels. A stretched frequency (0.012 0.05) makes horizontal wave bands, and animating it over 7s keeps the surface moving.
<span aria-hidden="true" class="absolute inset-[.15em_.12em] text-tth29-acc
opacity-0 [filter:url(#tth29-ripple)] transition-opacity duration-600
group-hover/word:opacity-100">UNDERTOW</span>Crossfade, not tween. Filters referenced by URL are on/off — they can't interpolate. So the crisp word fades to 6% while the permanently rippled clone fades in, which reads as the text slipping under water.
Scoped ids. SVG ids are document-global, so the filter is tth29-ripple.
Make it yours
- Rougher sea: raise
scaleon the displacement map from 16 to 28. - Calmer: lower
numOctavesto 1 and slow the animate to 12s. - Heat shimmer: use
baseFrequency="0.05 0.012"for vertical bands in a warm colour.
Gotchas — read before shipping
- The filter region (
x/y/width/height) must exceed the text or displaced edges clip. - Safari renders SVG filters on HTML on the CPU — keep the clone to one word.
- The SVG that holds the filter must be in the DOM (here at 0×0), not
display:none.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
SVG filters on HTML content work in all engines; SMIL animate is supported everywhere.