Tailwind v4MIT licensed

Tailwind Liquid Glass Card with Hover Refraction

The premium product card, ported so the whole material is legible in one class list: backdrop-blur-[18px] backdrop-saturate-180 is the frost, bg-white/12 plus border-white/35 plus an inset hairline shadow is the thickness, and a before: radial gloss reads two custom properties the pointer handler writes. The 3D tilt is an arbitrary [transform:rotateX(var(--ry))_rotateY(var(--rx))], so the JS never touches a style beyond four variables.

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 class="lgt-01 grid w-full min-h-svh place-items-center p-15 font-lgt01-sans [perspective:1100px] bg-[image:url('https://picsum.photos/seed/lgcard/1200/900')] bg-cover bg-center bg-fixed" aria-label="Tailwind liquid glass card with hover refraction">
  <article id="lgt-01-card" tabindex="0" class="group relative isolate w-[min(340px,90vw)] cursor-pointer overflow-hidden rounded-[26px] border border-white/35 bg-white/12 p-6.5 pb-5.5 text-white backdrop-blur-[18px] backdrop-saturate-180 shadow-[0_30px_60px_-30px_rgba(0,0,0,.65),inset_0_1px_0_rgba(255,255,255,.5)] transition-[transform,box-shadow] duration-500 ease-lgt01 [transform-style:preserve-3d] [transform:rotateX(var(--ry,0deg))_rotateY(var(--rx,0deg))] hover:shadow-[0_40px_80px_-28px_rgba(0,0,0,.7),inset_0_1px_0_rgba(255,255,255,.6)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-white/90 focus-visible:shadow-[0_40px_80px_-28px_rgba(0,0,0,.7),inset_0_1px_0_rgba(255,255,255,.6)] motion-reduce:transition-[box-shadow] motion-reduce:duration-200 before:pointer-events-none before:absolute before:inset-0 before:z-2 before:rounded-[inherit] before:transition-opacity before:duration-300 before:[background:radial-gradient(220px_220px_at_var(--mx,30%)_var(--my,20%),rgba(255,255,255,.55),rgba(255,255,255,0)_60%)] before:[opacity:var(--gloss,.5)] after:pointer-events-none after:absolute after:inset-0 after:z-1 after:rounded-[inherit] after:[background:linear-gradient(120deg,transparent_30%,rgba(255,255,255,.18)_46%,transparent_62%)] motion-reduce:after:hidden">
    <span class="relative z-3 inline-block rounded-full border border-white/40 bg-white/22 px-2.75 py-1.25 text-[11px] font-extrabold uppercase tracking-[0.12em]">New</span>
    <h3 class="relative z-3 mt-16 mb-2 text-2xl font-extrabold tracking-[-0.02em]">Aurora Headphones</h3>
    <p class="relative z-3 text-sm leading-relaxed text-white [text-shadow:0_1px_8px_rgba(0,0,0,.45)]">Spatial audio, 40-hour battery, and a machined-glass shell that catches the light like the UI around it.</p>
    <div class="relative z-3 mt-5 flex items-center justify-between">
      <span class="text-[22px] font-extrabold">$299</span>
      <span class="rounded-full bg-white/90 px-4 py-2 text-sm font-semibold text-lgt01-deep">View &rarr;</span>
    </div>
  </article>
</section>
@import "tailwindcss";

@theme {
  --color-lgt01-deep: #1a1330;
  --color-lgt01-deep: oklch(0.26 0.07 300);
  --font-lgt01-sans: 'Manrope', system-ui, sans-serif;
  --ease-lgt01: cubic-bezier(0.2, 0.9, 0.2, 1);
}
(() => {
  const card = document.querySelector('#lgt-01-card');
  if (!card) return;
  const MAX_TILT = 8;
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const onMove = e => {
    const r = card.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width;
    const py = (e.clientY - r.top) / r.height;
    card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
    card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
    card.style.setProperty('--gloss', '.85');
    if (reduce) return;
    card.style.setProperty('--rx', ((px - .5) * MAX_TILT * 2).toFixed(2) + 'deg');
    card.style.setProperty('--ry', (-(py - .5) * MAX_TILT * 2).toFixed(2) + 'deg');
  };
  const onLeave = () => {
    card.style.setProperty('--gloss', '.5');
    card.style.setProperty('--rx', '0deg');
    card.style.setProperty('--ry', '0deg');
  };
  card.addEventListener('pointermove', onMove);
  card.addEventListener('pointerleave', onLeave);
  card.cleanup = () => { card.removeEventListener('pointermove', onMove); card.removeEventListener('pointerleave', onLeave); };
})();

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-lgt01-deep: #1a1330;
  --color-lgt01-deep: oklch(0.26 0.07 300);
  --font-lgt01-sans: 'Manrope', system-ui, sans-serif;
  --ease-lgt01: cubic-bezier(0.2, 0.9, 0.2, 1);
}
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 four-utility glass recipe — bg-white/12, border-white/35, backdrop-blur-[18px] backdrop-saturate-180, inset hairline shadow — replaces a 6-declaration CSS rule and is reused verbatim in all twenty demos.
  • Both pseudo-element layers move into before: / after: variants, so the gloss and the specular sheen are visible on the element that owns them.
  • The script sets custom properties only. It never writes a style that carries design intent, which keeps the whole appearance in the class list where the compiler and the reader can both see it.
  • [perspective:1100px] and [transform-style:preserve-3d] stay arbitrary — they are scene plumbing, not design-scale values.
  • Safari's -webkit-backdrop-filter prefix is emitted by Tailwind automatically; the hand-written CSS version had to declare it twice everywhere.
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 Liquid Glass Card with Hover Refraction
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-glass-card-with-hover-refraction/

The premium product card, ported so the whole material is legible in one class list: backdrop-blur-[18px] backdrop-saturate-180 is the frost, bg-white/12 plus border-white/35 plus an inset hairline shadow is the thickness, and a before: radial gloss reads two custom properties the pointer handler writes. The 3D tilt is an arbitrary [transform:rotateX(var(--ry))_rotateY(var(--rx))], so the JS never touches a style beyond four variables.
## HTML
```html
<section class="lgt-01 grid w-full min-h-svh place-items-center p-15 font-lgt01-sans [perspective:1100px] bg-[image:url('https://picsum.photos/seed/lgcard/1200/900')] bg-cover bg-center bg-fixed" aria-label="Tailwind liquid glass card with hover refraction">
  <article id="lgt-01-card" tabindex="0" class="group relative isolate w-[min(340px,90vw)] cursor-pointer overflow-hidden rounded-[26px] border border-white/35 bg-white/12 p-6.5 pb-5.5 text-white backdrop-blur-[18px] backdrop-saturate-180 shadow-[0_30px_60px_-30px_rgba(0,0,0,.65),inset_0_1px_0_rgba(255,255,255,.5)] transition-[transform,box-shadow] duration-500 ease-lgt01 [transform-style:preserve-3d] [transform:rotateX(var(--ry,0deg))_rotateY(var(--rx,0deg))] hover:shadow-[0_40px_80px_-28px_rgba(0,0,0,.7),inset_0_1px_0_rgba(255,255,255,.6)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-white/90 focus-visible:shadow-[0_40px_80px_-28px_rgba(0,0,0,.7),inset_0_1px_0_rgba(255,255,255,.6)] motion-reduce:transition-[box-shadow] motion-reduce:duration-200 before:pointer-events-none before:absolute before:inset-0 before:z-2 before:rounded-[inherit] before:transition-opacity before:duration-300 before:[background:radial-gradient(220px_220px_at_var(--mx,30%)_var(--my,20%),rgba(255,255,255,.55),rgba(255,255,255,0)_60%)] before:[opacity:var(--gloss,.5)] after:pointer-events-none after:absolute after:inset-0 after:z-1 after:rounded-[inherit] after:[background:linear-gradient(120deg,transparent_30%,rgba(255,255,255,.18)_46%,transparent_62%)] motion-reduce:after:hidden">
    <span class="relative z-3 inline-block rounded-full border border-white/40 bg-white/22 px-2.75 py-1.25 text-[11px] font-extrabold uppercase tracking-[0.12em]">New</span>
    <h3 class="relative z-3 mt-16 mb-2 text-2xl font-extrabold tracking-[-0.02em]">Aurora Headphones</h3>
    <p class="relative z-3 text-sm leading-relaxed text-white [text-shadow:0_1px_8px_rgba(0,0,0,.45)]">Spatial audio, 40-hour battery, and a machined-glass shell that catches the light like the UI around it.</p>
    <div class="relative z-3 mt-5 flex items-center justify-between">
      <span class="text-[22px] font-extrabold">$299</span>
      <span class="rounded-full bg-white/90 px-4 py-2 text-sm font-semibold text-lgt01-deep">View &rarr;</span>
    </div>
  </article>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-lgt01-deep: #1a1330;
  --color-lgt01-deep: oklch(0.26 0.07 300);
  --font-lgt01-sans: 'Manrope', system-ui, sans-serif;
  --ease-lgt01: cubic-bezier(0.2, 0.9, 0.2, 1);
}
```

## JavaScript
```js
(() => {
  const card = document.querySelector('#lgt-01-card');
  if (!card) return;
  const MAX_TILT = 8;
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const onMove = e => {
    const r = card.getBoundingClientRect();
    const px = (e.clientX - r.left) / r.width;
    const py = (e.clientY - r.top) / r.height;
    card.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
    card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
    card.style.setProperty('--gloss', '.85');
    if (reduce) return;
    card.style.setProperty('--rx', ((px - .5) * MAX_TILT * 2).toFixed(2) + 'deg');
    card.style.setProperty('--ry', (-(py - .5) * MAX_TILT * 2).toFixed(2) + 'deg');
  };
  const onLeave = () => {
    card.style.setProperty('--gloss', '.5');
    card.style.setProperty('--rx', '0deg');
    card.style.setProperty('--ry', '0deg');
  };
  card.addEventListener('pointermove', onMove);
  card.addEventListener('pointerleave', onLeave);
  card.cleanup = () => { card.removeEventListener('pointermove', onMove); card.removeEventListener('pointerleave', onLeave); };
})();
```

How this works

The material is four utilities. A translucent fill, a hairline border, an inset rim-light and a paired blur/saturate. Written together they are the entire Liquid Glass surface, and every other demo in this collection reuses the same four:

class="bg-white/12 border border-white/35
       backdrop-blur-[18px] backdrop-saturate-180
       shadow-[0_30px_60px_-30px_rgba(0,0,0,.65),inset_0_1px_0_rgba(255,255,255,.5)]"

The gloss is a pseudo-element variant, not a child div. before: paints a radial highlight positioned from --mx / --my, and its opacity reads --gloss. Because the position lives in variables, the pointer handler writes numbers rather than CSS, and the compiler still sees a static class string.

Tilt is an arbitrary transform. [transform:rotateX(var(--ry,0deg))_rotateY(var(--rx,0deg))] inside a [perspective:1100px] parent. The underscores are the space escape; they compile to real spaces. On pointerleave the variables reset and the transition springs the card back.

Isolation is not optional. isolate gives the card its own stacking context so its backdrop-filter samples the page background and not its siblings — the single most common cause of muddy glass when several frosted cards sit in a grid.

Make it yours

  • Push the frost with backdrop-blur-[24px], but treat 24px as the ceiling — beyond it the paint cost starts showing up in INP on mid-range Android.
  • Retint the glass by editing the opacity modifier: bg-white/12 → bg-white/20 for a milkier surface, or bg-black/20 for smoked glass on light backgrounds.
  • Tune tilt with the MAX_TILT constant in the script; 8 degrees is the point where it still reads as glass rather than as a toy.
  • Change the gloss size and colour in the before:[background:radial-gradient(...)] arbitrary value.
  • Add a second accent by giving the card a --color-lgt01-accent theme entry and using it on the CTA capsule.

Gotchas — read before shipping

  • backdrop-filter needs something non-opaque behind it. Over a flat bg-slate-900 the blur has nothing to sample and the card looks like a plain tinted box — put it over an image or a gradient.
  • Tailwind emits -webkit-backdrop-filter alongside the standard property, so the Safari prefix you hand-wrote in CSS is no longer your problem.
  • Without isolate a frosted card samples its frosted siblings, and a grid of them turns to mush after the second row.
  • Arbitrary values cannot contain spaces — use the underscore escape. A literal space silently ends the class and the whole declaration disappears.
  • Class strings must be statically visible to the compiler; building [transform:rotate(${deg}deg)] at runtime produces nothing. Write to a custom property instead, as the script here does.
  • Body text over glass fails WCAG fast. Keep it at full opacity with a text-shadow, never text-white/60 over a photograph.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Tailwind v4's generated sheet sets the floor (cascade layers, @property, color-mix). backdrop-filter itself goes back to Chrome 76 / Safari 14 / Firefox 103, and the pointer tilt is progressive enhancement — with no JS the card is a static, fully readable glass panel.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…