---
name: liquid-text-effect
description: Use when you want "Experimental, playful, exotic" - Makes text appear fluid, wavy, or melting.
---

# Liquid Text Effect

> **Category:** Text  -  **Personality:** Experimental, playful, exotic
>
> **Best use:** Creative sites, music, art
>
> **Ratings:** Professional 2/5 - Casual 4/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 5/5 - Performance cost 4/5

## What it is

The Liquid Text Effect makes a headline look like it is flowing, rippling or slowly melting. It is built entirely from an inline SVG filter: `feTurbulence` generates animated fractal noise and `feDisplacementMap` uses that noise to shove the text's pixels sideways, so the letterforms warp and churn continuously. Because the headline stays as real SVG `<text>` (vector, gradient-filled, with a visually-hidden text copy for assistive tech), it remains razor-sharp at any size while appearing fluid. It is an exotic, attention-grabbing treatment best reserved for creative, music and art sites where the headline itself is the show.

## Dependencies / CDN

None for the effect itself — it is pure inline SVG + CSS, plus a few lines of vanilla JS for the optional pointer interaction. The demo loads a heavy display face (the liquid look needs thick glyphs) and a body font — optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700;800;900&family=Shrikhand&display=swap" rel="stylesheet">
```

## HTML

The whole recipe is the off-screen `<defs>` (the gradient + the liquid filter) plus a headline whose `<text>` references both. A visually-hidden `.lq-sr` span carries the real, accessible words.

```html
<!-- reusable, off-screen filter + gradient definitions -->
<svg class="lq-defs" aria-hidden="true" focusable="false"><defs>
  <linearGradient id="lq-grad" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#ff36c7"/><stop offset=".42" stop-color="#22e0d6"/><stop offset="1" stop-color="#c4ff45"/>
    <animateTransform attributeName="gradientTransform" type="rotate" from="0 .5 .5" to="360 .5 .5" dur="26s" repeatCount="indefinite"/>
  </linearGradient>
  <!-- LIQUID: turbulent noise displaces the text; the baseFrequency morph IS the flow -->
  <filter id="lq-liquid" x="-20%" y="-35%" width="140%" height="180%" color-interpolation-filters="sRGB">
    <feTurbulence type="fractalNoise" baseFrequency="0.009 0.016" numOctaves="2" seed="8" result="noise">
      <animate attributeName="baseFrequency" dur="20s" calcMode="spline" keyTimes="0;0.5;1"
        keySplines="0.45 0 0.55 1;0.45 0 0.55 1" values="0.009 0.016;0.014 0.010;0.009 0.016" repeatCount="indefinite"/>
    </feTurbulence>
    <feDisplacementMap id="lq-disp" in="SourceGraphic" in2="noise" scale="14" xChannelSelector="R" yChannelSelector="G"/>
  </filter>
</defs></svg>

<!-- the liquid headline: real vector text, gradient-painted, warped by the filter -->
<h1 class="lq-h">
  <span class="lq-sr">liquid sound system</span>
  <svg class="lq-wordmark" viewBox="0 0 700 240" role="img" aria-hidden="true" focusable="false">
    <text class="lq-wordtext" x="50%" y="62%" text-anchor="middle" dominant-baseline="middle"
          fill="url(#lq-grad)" filter="url(#lq-liquid)">liquid</text>
  </svg>
  <span class="lq-h2">sound system</span>
</h1>
```

## CSS

Almost nothing — the filter does the work. The two rules that matter: `overflow:visible` on the `<svg>` so the displaced pixels are not clipped, and a thick display font so there is plenty of glyph to melt.

```css
/* the headline mark: real SVG <text>, gradient-filled, warped by the filter.
   overflow:visible keeps displaced pixels from being clipped at the SVG box */
.lq-wordmark{display:block;width:100%;max-width:600px;height:auto;overflow:visible;cursor:crosshair}
.lq-wordtext{font-family:'Shrikhand',serif;font-size:152px}
.lq-h2{display:block;font-family:'Shrikhand',serif;font-weight:400;font-size:clamp(25px,4.3vw,40px);
  letter-spacing:.004em;margin-top:-4px;opacity:.96}

/* visually-hidden real-text copy for screen readers / SEO */
.lq-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* reduced motion: stop ambient animations (the JS also zeroes the warp + strips SMIL) */
@media(prefers-reduced-motion:reduce){
  .lq-blob,.lq-brand i,.lq-live::before,.lq-prog i{animation:none}
}
```

## JavaScript

Optional. The idle flow is the SMIL `baseFrequency` morph in the SVG — JS only nudges the displacement `scale` up as the pointer nears the headline and adds a decaying "splash" on click. It also fully settles the headline (and removes the SMIL nodes) under reduced motion.

```js
(function(){
  var stage=document.querySelector('.lq-stage');
  var disp=document.getElementById('lq-disp');
  var word=document.querySelector('.lq-wordmark');
  if(!stage||!disp||!word) return;

  // Reduced motion: settle to a crisp, fully readable headline + stop the SMIL.
  if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches){
    disp.setAttribute('scale','0');
    stage.querySelectorAll('animate,animateTransform').forEach(function(a){
      try{ a.parentNode.removeChild(a); }catch(e){}
    });
    return;
  }

  var BASE=12, MAX=38, cur=BASE, px=null, py=null, prox=0, splash=0;

  stage.addEventListener('pointermove',function(e){ px=e.clientX; py=e.clientY; });
  stage.addEventListener('pointerleave',function(){ px=py=null; });
  stage.addEventListener('pointerdown',function(){ splash=Math.min(splash+24,30); });

  (function frame(){
    if(px!==null){
      var r=word.getBoundingClientRect();
      var dx=px-(r.left+r.width/2), dy=py-(r.top+r.height/2);
      var d=Math.sqrt(dx*dx+dy*dy);
      prox=Math.max(0, 1 - d/Math.max(r.width,360));
    } else { prox*=0.9; }
    var target=Math.min(MAX, BASE + prox*22 + splash);
    cur += (target-cur)*0.12;          // lerp toward target = smooth easing
    splash *= 0.9;                      // splash decays each frame
    disp.setAttribute('scale', cur.toFixed(2));
    requestAnimationFrame(frame);
  })();
})();
```

## How it works

- **`feTurbulence`** synthesises an animated fractal-noise image. Its `baseFrequency` controls the "zoom" of the noise (low = big slow swells, high = tight ripples); `numOctaves` adds finer detail; `seed` picks the pattern.
- **`feDisplacementMap`** is the engine: for every pixel of the `SourceGraphic` (the text) it reads the noise's red channel for a horizontal offset and green channel for a vertical offset, then pushes that pixel by `scale` px. Smoothly-varying noise → smoothly-warped letters.
- **The flow is the SMIL `<animate>`** on `baseFrequency`: morphing the noise field over 20s makes the displacement field crawl, so the text appears to ooze on its own — no JS required.
- **The rotating `<linearGradient>`** (`animateTransform … rotate`) fills the glyphs, so the liquid also shifts colour through magenta → cyan → lime.
- **The headline stays real text.** It is genuine SVG `<text>`, so it is crisp/vector at any size; an `aria-hidden` mark plus a visually-hidden `.lq-sr` span gives assistive tech and crawlers the actual words.
- **JS just nudges intensity.** A `requestAnimationFrame` loop lerps `feDisplacementMap`'s `scale` upward as the pointer nears the wordmark and adds a decaying "splash" on click. (The demo also runs a second `feGaussianBlur` + `feColorMatrix` "goo" filter to merge the visualiser dots into metaballs — the same family of filter trick.)

## Customization

- **Flow speed:** the `<animate>` `dur` on `baseFrequency` (and the gradient's rotate `dur`). Longer = lazier ooze.
- **Warp amount:** `feDisplacementMap` `scale` (static), or the JS `BASE`/`MAX` bounds. ~8–18 reads as gentle liquid; 30+ starts to tear the letters apart.
- **Noise character:** `baseFrequency` (low = big rolling waves, high = jittery ripples), `numOctaves` for detail, `seed` to reshuffle. The two `values` in the `<animate>` set how far the field morphs.
- **Colour:** edit the `<linearGradient>` stops, or drop `fill="url(#lq-grad)"` for a flat `fill` colour.
- **Font:** any thick display face works (Shrikhand here); the heavier the glyphs, the more surface there is to melt. Tune `font-size` on `.lq-wordtext`.
- **Interaction:** remove the JS entirely for a pure self-animating headline, or change `prox*22` / the click `splash` to make the pointer pull stronger or weaker.

## Accessibility & performance

- **Always provide real text.** The warped mark is `aria-hidden`; pair it with a visually-hidden real-text copy (`.lq-sr`) so screen readers, copy/paste and SEO get the actual words.
- **SVG filters are expensive.** `feTurbulence` with an animated `baseFrequency` is one of the heavier filter primitives — it re-renders noise every frame. Keep it to one or two small headline-sized elements; never filter long body copy or a large region.
- **Reduced motion:** honour `prefers-reduced-motion`. The JS here sets `feDisplacementMap` `scale` to `0` and deletes the `<animate>`/`<animateTransform>` nodes, so the headline resolves to crisp, fully legible text with no churn.
- **Animate the right thing.** The JS eases `scale` via `requestAnimationFrame` + a lerp rather than thrashing layout; that keeps the interaction smooth.

## Gotchas

- **Clipping.** Displacement pushes pixels outside the element's box. Give the `<filter>` a roomy region (negative `x`/`y`, `width`/`height` > 100% — here `-20% / -35% / 140% / 180%`) **and** set `overflow:visible` on the `<svg>`, or you will see the liquid get sliced off at hard edges.
- **`color-interpolation-filters="sRGB"`.** Filters default to linearRGB, which shifts colours and darkens the result. Set `sRGB` on the `<filter>` (done in both filters here) to keep colours true.
- **Input wiring.** `feDisplacementMap` needs `in="SourceGraphic"` and `in2="noise"`, where `noise` is the `result` of the `feTurbulence` that precedes it — order and the `result`/`in2` names must match.
- **SMIL support.** The self-animation relies on SMIL `<animate>` (no IE; modern Chrome/Firefox/Safari are fine). If you need a guaranteed fallback, drive `baseFrequency` from JS instead — and note the reduced-motion path here works by *removing* the SMIL nodes.
- **Legibility vs. drama.** Big `scale` + high `baseFrequency` quickly renders the word unreadable. Tune them together, keep it to short display words, and never apply it to text users must actually read.
