Tailwind v4MIT licensed

Tailwind SVG Displacement Liquid Glass Background

The real thing: feTurbulence plus feDisplacementMap physically bending the image beneath the glass, with warp intensity following the cursor. Tailwind styles the panel, the stage and the layout; the filter stays SVG because displacement is geometry, not styling — and that division is exactly the point of the port.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section id="lgt-08-root" class="lgt-08 relative grid w-full min-h-svh place-items-center bg-lgt08-bg p-15 font-lgt08-sans" aria-label="Tailwind SVG displacement liquid glass background">
  <svg id="lgt-08-svg" viewBox="0 0 600 380" width="600" height="380" preserveAspectRatio="xMidYMid slice" aria-hidden="true" class="pointer-events-none absolute col-start-1 row-start-1 h-auto w-[min(720px,94vw)] overflow-hidden rounded-[26px] saturate-120 shadow-[0_40px_80px_-34px_rgba(0,0,0,.8)]">
    <defs>
      <filter id="lgt-08-warp" x="-20%" y="-20%" width="140%" height="140%">
        <feTurbulence id="lgt-08-turb" type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="noise"/>
        <feDisplacementMap id="lgt-08-disp" in="SourceGraphic" in2="noise" scale="14" xChannelSelector="R" yChannelSelector="G"/>
      </filter>
    </defs>
    <image href="https://picsum.photos/seed/lgwarp/600/380" x="0" y="0" width="600" height="380" preserveAspectRatio="xMidYMid slice" filter="url(#lgt-08-warp)"/>
  </svg>

  <div class="relative z-2 col-start-1 row-start-1 w-[min(360px,84vw)] rounded-[22px] border border-white/32 bg-white/10 p-7 text-white backdrop-blur-[6px] backdrop-saturate-150 shadow-[inset_0_1px_0_rgba(255,255,255,.45),0_20px_50px_-26px_rgba(0,0,0,.6)]">
    <span class="inline-block rounded-full border border-white/35 bg-white/20 px-2.75 py-1.25 text-[11px] font-bold uppercase tracking-[0.1em]">SVG feDisplacementMap</span>
    <h3 class="mt-3.5 mb-2 text-[28px] font-extrabold tracking-[-0.02em]">Real refraction</h3>
    <p class="text-[15px] leading-relaxed text-white [text-shadow:0_1px_8px_rgba(0,0,0,.45)]">Move your cursor &mdash; the image bends through the glass. This is displacement, not blur.</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-lgt08-bg: #0c0a16;
  --font-lgt08-sans: 'Manrope', system-ui, sans-serif;
}
(() => {
  const root = document.querySelector('#lgt-08-root');
  const disp = root.querySelector('#lgt-08-disp');
  const turb = root.querySelector('#lgt-08-turb');
  const svg = root.querySelector('#lgt-08-svg');
  if (!disp) return;
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const BASE = 14, MAX = 40;
  let target = BASE, current = BASE, raf = null;

  const tick = () => {
    current += (target - current) * 0.12;
    disp.setAttribute('scale', current.toFixed(2));
    if (Math.abs(target - current) > 0.2) { raf = requestAnimationFrame(tick); }
    else { disp.setAttribute('scale', target.toFixed(2)); raf = null; }
  };
  const kick = () => { if (!raf) raf = requestAnimationFrame(tick); };

  root.addEventListener('pointermove', e => {
    if (reduce) return;
    const r = svg.getBoundingClientRect();
    const dx = Math.abs((e.clientX - (r.left + r.width / 2)) / (r.width / 2));
    const dy = Math.abs((e.clientY - (r.top + r.height / 2)) / (r.height / 2));
    const t = Math.min(1, (dx + dy) / 2);
    target = BASE + t * (MAX - BASE);
    const f = 0.012 + t * 0.02;
    turb.setAttribute('baseFrequency', f.toFixed(4) + ' ' + (f * 1.6).toFixed(4));
    kick();
  });
  root.addEventListener('pointerleave', () => { target = BASE; kick(); });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-lgt08-bg: #0c0a16;
  --font-lgt08-sans: 'Manrope', system-ui, sans-serif;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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=Manrope:wght@400;600;800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The filter stays SVG and the layout becomes utilities — a clean division: Tailwind owns the surface, SVG owns the physics.
  • grid place-items-center with both children on col-start-1 row-start-1 stacks the warped image and the panel without absolute positioning.
  • The panel's blur is deliberately low; heavy frost over a displaced image erases the very effect the demo is built to show.
  • Filter ids carry the lgt-08- namespace because SVG filter references are document-global and collisions fail silently.
  • The rAF easing loop decouples pointer frequency from paint frequency, which is what keeps a full-region filter INP-safe.
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 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 Displacement Liquid Glass Background
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-svg-displacement-liquid-glass-background/

The real thing: feTurbulence plus feDisplacementMap physically bending the image beneath the glass, with warp intensity following the cursor. Tailwind styles the panel, the stage and the layout; the filter stays SVG because displacement is geometry, not styling &mdash; and that division is exactly the point of the port.
## HTML
```html
<section id="lgt-08-root" class="lgt-08 relative grid w-full min-h-svh place-items-center bg-lgt08-bg p-15 font-lgt08-sans" aria-label="Tailwind SVG displacement liquid glass background">
  <svg id="lgt-08-svg" viewBox="0 0 600 380" width="600" height="380" preserveAspectRatio="xMidYMid slice" aria-hidden="true" class="pointer-events-none absolute col-start-1 row-start-1 h-auto w-[min(720px,94vw)] overflow-hidden rounded-[26px] saturate-120 shadow-[0_40px_80px_-34px_rgba(0,0,0,.8)]">
    <defs>
      <filter id="lgt-08-warp" x="-20%" y="-20%" width="140%" height="140%">
        <feTurbulence id="lgt-08-turb" type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="noise"/>
        <feDisplacementMap id="lgt-08-disp" in="SourceGraphic" in2="noise" scale="14" xChannelSelector="R" yChannelSelector="G"/>
      </filter>
    </defs>
    <image href="https://picsum.photos/seed/lgwarp/600/380" x="0" y="0" width="600" height="380" preserveAspectRatio="xMidYMid slice" filter="url(#lgt-08-warp)"/>
  </svg>

  <div class="relative z-2 col-start-1 row-start-1 w-[min(360px,84vw)] rounded-[22px] border border-white/32 bg-white/10 p-7 text-white backdrop-blur-[6px] backdrop-saturate-150 shadow-[inset_0_1px_0_rgba(255,255,255,.45),0_20px_50px_-26px_rgba(0,0,0,.6)]">
    <span class="inline-block rounded-full border border-white/35 bg-white/20 px-2.75 py-1.25 text-[11px] font-bold uppercase tracking-[0.1em]">SVG feDisplacementMap</span>
    <h3 class="mt-3.5 mb-2 text-[28px] font-extrabold tracking-[-0.02em]">Real refraction</h3>
    <p class="text-[15px] leading-relaxed text-white [text-shadow:0_1px_8px_rgba(0,0,0,.45)]">Move your cursor &mdash; the image bends through the glass. This is displacement, not blur.</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-lgt08-bg: #0c0a16;
  --font-lgt08-sans: 'Manrope', system-ui, sans-serif;
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('#lgt-08-root');
  const disp = root.querySelector('#lgt-08-disp');
  const turb = root.querySelector('#lgt-08-turb');
  const svg = root.querySelector('#lgt-08-svg');
  if (!disp) return;
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const BASE = 14, MAX = 40;
  let target = BASE, current = BASE, raf = null;

  const tick = () => {
    current += (target - current) * 0.12;
    disp.setAttribute('scale', current.toFixed(2));
    if (Math.abs(target - current) > 0.2) { raf = requestAnimationFrame(tick); }
    else { disp.setAttribute('scale', target.toFixed(2)); raf = null; }
  };
  const kick = () => { if (!raf) raf = requestAnimationFrame(tick); };

  root.addEventListener('pointermove', e => {
    if (reduce) return;
    const r = svg.getBoundingClientRect();
    const dx = Math.abs((e.clientX - (r.left + r.width / 2)) / (r.width / 2));
    const dy = Math.abs((e.clientY - (r.top + r.height / 2)) / (r.height / 2));
    const t = Math.min(1, (dx + dy) / 2);
    target = BASE + t * (MAX - BASE);
    const f = 0.012 + t * 0.02;
    turb.setAttribute('baseFrequency', f.toFixed(4) + ' ' + (f * 1.6).toFixed(4));
    kick();
  });
  root.addEventListener('pointerleave', () => { target = BASE; kick(); });
})();
```

How this works

Displacement is not blur, and this is the demo that proves it. feTurbulence generates a smooth fractal noise field; feDisplacementMap reads that noise and pushes each pixel of the image sideways and vertically by scale. The result bends edges the way real rippled glass does — something no amount of backdrop-blur can imitate.

Tailwind's job here is everything around the filter. The stage, the panel, the rim, the type. The filter's job is the refraction. Keeping that boundary clean is why the port stays readable:

<!-- utilities: the glass panel -->
class="bg-white/10 border border-white/32
       backdrop-blur-[6px] backdrop-saturate-150"

<!-- SVG: the physics -->
<feDisplacementMap scale="14" xChannelSelector="R" yChannelSelector="G"/>

The pointer handler eases rather than tracks. Each pointermove sets a target; a requestAnimationFrame loop moves the current value 12% of the way there per frame and stops when it arrives. Pointer events can fire faster than frames, so this keeps filter re-evaluation bounded and protects INP.

The panel is barely frosted on purpose. A heavy blur over a displaced image erases the displacement. 6px of blur plus a strong rim reads as glass while leaving the warp legible.

Make it yours

  • Change the ripple's character with baseFrequency: low values give big lazy waves, high values give tight ripples. numOctaves=&quot;2&quot; is the sweet spot for cost versus richness.
  • Set the resting and peak warp with the BASE and MAX constants; above roughly 40 the image stops reading as an image.
  • Add an SVG <animate> on baseFrequency for a glass that flows continuously even without a pointer.
  • Swap the image seed for your own art — keep preserveAspectRatio=&quot;xMidYMid slice&quot; so it fills without distorting.
  • Raise the panel's blur if you want a more conventional frosted card, accepting that you are trading away the refraction you built.

Gotchas — read before shipping

  • Displacement is GPU work but not free. Cap scale and throttle updates with rAF, or a large filtered region will cost you INP on mid-range hardware.
  • Filter the image, never the panel. Applying the warp to a container that holds text turns your UI into liquid nonsense.
  • SVG filter ids are document-global. Two un-namespaced #warp filters resolve to the first and the second element silently gets the wrong distortion.
  • Setting scale via setAttribute is correct here — it is an SVG attribute, not a CSS property, and no Tailwind utility can reach it.
  • Keep a sensible resting state. If the whole effect only exists on pointermove, touch users see a flat image.
  • Under reduced motion, stop updating the filter entirely rather than easing more slowly — rippling geometry is precisely what the query is asking you to stop.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

SVG filters are universally supported and long predate Tailwind; the v4 sheet sets the floor. The cursor-reactive warp is progressive enhancement — with no JS the panel already floats over a permanently refracted image.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…