Tailwind v4MIT licensed

Tailwind Liquid Glass Modal Dialog

A dialog that frosts the entire page behind it, built on native <dialog> so focus containment and Escape-to-close come for free. The port's headline is the backdrop: variant — Tailwind styles ::backdrop directly, so the scrim's blur is a utility on the same element as the panel's.

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-05 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt05-bg font-lgt05-sans" aria-label="Tailwind liquid glass modal dialog">
  <img src="https://picsum.photos/seed/lgmodal/1200/800" width="1200" height="800" alt="Studio workspace" class="absolute inset-0 z-0 size-full object-cover">
  <button id="lgt-05-open" type="button" class="relative z-1 cursor-pointer rounded-full border border-white/40 bg-white/16 px-7.5 py-3.75 text-base font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 shadow-[inset_0_1px_0_rgba(255,255,255,.5),0_14px_30px_-16px_rgba(0,0,0,.6)] transition-transform duration-200 hover:-translate-y-0.5 focus-visible:-translate-y-0.5 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white motion-reduce:transition-none">Open glass dialog</button>

  <dialog id="lgt-05-dialog" aria-labelledby="lgt-05-title" class="max-w-[min(420px,92vw)] rounded-[26px] border border-white/35 bg-white/16 p-7.5 text-white backdrop-blur-[24px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.5),0_40px_80px_-30px_rgba(0,0,0,.7)] open:animate-lgt05-in data-closing:animate-lgt05-out backdrop:bg-lgt05-scrim backdrop:backdrop-blur-[14px] backdrop:backdrop-saturate-140 motion-reduce:animate-none motion-reduce:data-closing:animate-none">
    <h3 id="lgt-05-title" class="mb-2.5 text-[22px] font-extrabold">Confirm upgrade</h3>
    <p class="text-[15px] leading-relaxed text-white [text-shadow:0_1px_6px_rgba(0,0,0,.35)]">You're switching to the Studio plan. Your new features unlock immediately and billing adjusts on your next cycle.</p>
    <div class="mt-6 flex justify-end gap-3">
      <button id="lgt-05-cancel" type="button" class="cursor-pointer rounded-full border border-white/35 bg-white/12 px-5 py-2.75 text-sm font-bold text-white transition-[background-color,transform] duration-200 hover:bg-white/22 focus-visible:bg-white/22 active:scale-96 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none">Cancel</button>
      <button id="lgt-05-confirm" type="button" class="cursor-pointer rounded-full border border-lgt05-accent/60 bg-lgt05-accent px-5 py-2.75 text-sm font-bold text-white transition-[background-color,transform] duration-200 hover:brightness-110 focus-visible:brightness-110 active:scale-96 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none">Upgrade</button>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-lgt05-bg: #0e0b1a;
  --color-lgt05-bg: oklch(0.16 0.03 295);
  --color-lgt05-accent: #6a4bff;
  --color-lgt05-accent: oklch(0.62 0.2 275);
  --color-lgt05-scrim: rgb(10 8 20 / 0.4);
  --font-lgt05-sans: 'Manrope', system-ui, sans-serif;
  --animate-lgt05-in: lgt-05-in 0.35s cubic-bezier(0.2, 0.9, 0.2, 1);
  --animate-lgt05-out: lgt-05-out 0.2s ease forwards;
}

@keyframes lgt-05-in {
  from {
    transform: scale(0.9);
    opacity: 0;
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes lgt-05-out {
  to {
    transform: scale(0.95);
    opacity: 0;
  }
}
(() => {
  const root = document.querySelector('.lgt-05');
  const dlg = root.querySelector('#lgt-05-dialog');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const close = () => {
    if (reduce) { dlg.close(); return; }
    dlg.dataset.closing = '';
    dlg.addEventListener('animationend', () => { delete dlg.dataset.closing; dlg.close(); }, { once: true });
  };
  root.querySelector('#lgt-05-open').addEventListener('click', () => dlg.showModal());
  root.querySelector('#lgt-05-cancel').addEventListener('click', close);
  root.querySelector('#lgt-05-confirm').addEventListener('click', close);
  dlg.addEventListener('click', e => {
    const r = dlg.getBoundingClientRect();
    if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) close();
  });
  dlg.addEventListener('cancel', e => { e.preventDefault(); close(); });
})();

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-lgt05-bg: #0e0b1a;
  --color-lgt05-bg: oklch(0.16 0.03 295);
  --color-lgt05-accent: #6a4bff;
  --color-lgt05-accent: oklch(0.62 0.2 275);
  --color-lgt05-scrim: rgb(10 8 20 / 0.4);
  --font-lgt05-sans: 'Manrope', system-ui, sans-serif;
  --animate-lgt05-in: lgt-05-in 0.35s cubic-bezier(0.2, 0.9, 0.2, 1);
  --animate-lgt05-out: lgt-05-out 0.2s ease forwards;
}

@keyframes lgt-05-in {
  from {
    transform: scale(0.9);
    opacity: 0;
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes lgt-05-out {
  to {
    transform: scale(0.95);
    opacity: 0;
  }
}
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.

  • backdrop: styles the native ::backdrop pseudo-element, so the frosted scrim needs no wrapper div — the blur is a utility on the dialog itself.
  • open: reads the browser-managed attribute, and the exit state is a data-closing attribute styled by data-closing: — the script never applies a class that carries design.
  • Both keyframes are --animate-* theme entries, which is what turns them into single-word utilities.
  • showModal() supplies the focus trap, the top layer, Escape handling and background inertness; none of it is reimplemented.
  • Under reduced motion the script skips the animationend wait entirely — with animate-none the event never fires, and a dialog that will not close is worse than one that does not animate.
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 Modal Dialog
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-glass-modal-dialog/

A dialog that frosts the entire page behind it, built on native <dialog> so focus containment and Escape-to-close come for free. The port's headline is the backdrop: variant &mdash; Tailwind styles ::backdrop directly, so the scrim's blur is a utility on the same element as the panel's.
## HTML
```html
<section class="lgt-05 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt05-bg font-lgt05-sans" aria-label="Tailwind liquid glass modal dialog">
  <img src="https://picsum.photos/seed/lgmodal/1200/800" width="1200" height="800" alt="Studio workspace" class="absolute inset-0 z-0 size-full object-cover">
  <button id="lgt-05-open" type="button" class="relative z-1 cursor-pointer rounded-full border border-white/40 bg-white/16 px-7.5 py-3.75 text-base font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 shadow-[inset_0_1px_0_rgba(255,255,255,.5),0_14px_30px_-16px_rgba(0,0,0,.6)] transition-transform duration-200 hover:-translate-y-0.5 focus-visible:-translate-y-0.5 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white motion-reduce:transition-none">Open glass dialog</button>

  <dialog id="lgt-05-dialog" aria-labelledby="lgt-05-title" class="max-w-[min(420px,92vw)] rounded-[26px] border border-white/35 bg-white/16 p-7.5 text-white backdrop-blur-[24px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.5),0_40px_80px_-30px_rgba(0,0,0,.7)] open:animate-lgt05-in data-closing:animate-lgt05-out backdrop:bg-lgt05-scrim backdrop:backdrop-blur-[14px] backdrop:backdrop-saturate-140 motion-reduce:animate-none motion-reduce:data-closing:animate-none">
    <h3 id="lgt-05-title" class="mb-2.5 text-[22px] font-extrabold">Confirm upgrade</h3>
    <p class="text-[15px] leading-relaxed text-white [text-shadow:0_1px_6px_rgba(0,0,0,.35)]">You're switching to the Studio plan. Your new features unlock immediately and billing adjusts on your next cycle.</p>
    <div class="mt-6 flex justify-end gap-3">
      <button id="lgt-05-cancel" type="button" class="cursor-pointer rounded-full border border-white/35 bg-white/12 px-5 py-2.75 text-sm font-bold text-white transition-[background-color,transform] duration-200 hover:bg-white/22 focus-visible:bg-white/22 active:scale-96 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none">Cancel</button>
      <button id="lgt-05-confirm" type="button" class="cursor-pointer rounded-full border border-lgt05-accent/60 bg-lgt05-accent px-5 py-2.75 text-sm font-bold text-white transition-[background-color,transform] duration-200 hover:brightness-110 focus-visible:brightness-110 active:scale-96 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none">Upgrade</button>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-lgt05-bg: #0e0b1a;
  --color-lgt05-bg: oklch(0.16 0.03 295);
  --color-lgt05-accent: #6a4bff;
  --color-lgt05-accent: oklch(0.62 0.2 275);
  --color-lgt05-scrim: rgb(10 8 20 / 0.4);
  --font-lgt05-sans: 'Manrope', system-ui, sans-serif;
  --animate-lgt05-in: lgt-05-in 0.35s cubic-bezier(0.2, 0.9, 0.2, 1);
  --animate-lgt05-out: lgt-05-out 0.2s ease forwards;
}

@keyframes lgt-05-in {
  from {
    transform: scale(0.9);
    opacity: 0;
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes lgt-05-out {
  to {
    transform: scale(0.95);
    opacity: 0;
  }
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.lgt-05');
  const dlg = root.querySelector('#lgt-05-dialog');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const close = () => {
    if (reduce) { dlg.close(); return; }
    dlg.dataset.closing = '';
    dlg.addEventListener('animationend', () => { delete dlg.dataset.closing; dlg.close(); }, { once: true });
  };
  root.querySelector('#lgt-05-open').addEventListener('click', () => dlg.showModal());
  root.querySelector('#lgt-05-cancel').addEventListener('click', close);
  root.querySelector('#lgt-05-confirm').addEventListener('click', close);
  dlg.addEventListener('click', e => {
    const r = dlg.getBoundingClientRect();
    if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) close();
  });
  dlg.addEventListener('cancel', e => { e.preventDefault(); close(); });
})();
```

How this works

The scrim is a variant, not a second element. Most glass-modal tutorials add a wrapper div for the blurred overlay. With native <dialog> the browser already provides ::backdrop, and Tailwind exposes it:

class="backdrop:bg-lgt05-scrim
       backdrop:backdrop-blur-[14px]
       backdrop:backdrop-saturate-140"

State comes from the element, not from bookkeeping. open: keys off the attribute the browser sets when showModal() runs. The only thing the script adds is a data-closing attribute for the exit animation — and even that is styled by a data-closing: variant rather than by a class carrying utilities.

The panel is the same four-utility material at a heavier blur (backdrop-blur-[24px]), because a dialog sits above everything and can afford the paint. Body text stays at full white with a soft shadow so it holds contrast over any page beneath.

What is deliberately not here: a focus trap, an Escape listener, an inert-background hack, a portal. showModal() does all four, correctly, in every supporting browser.

Make it yours

  • Change the scrim's character in the backdrop: chain — heavier blur for focus, more tint for drama, or backdrop:backdrop-grayscale for a stark, editorial modal.
  • Restyle the entrance by editing the @keyframes behind --animate-lgt05-in; a slight rotate(-1deg) at the start gives it a card-being-dealt feel.
  • Widen for content-heavy dialogs with max-w-[min(640px,92vw)]; nothing else needs to change.
  • Recolour the confirm button from the single --color-lgt05-accent theme entry.
  • Add a close X in the corner as a real <button> with an aria-label — icon-only controls still need names.

Gotchas — read before shipping

  • Use <dialog> and showModal(). A hand-rolled div modal has to reimplement focus containment, Escape, the top layer and background inertness, and almost always gets at least one of them wrong.
  • ::backdrop only exists for dialogs opened with showModal(). Calling show() gives you a non-modal dialog with no backdrop at all.
  • Animate transform and opacity on the panel, never width or height — a resizing dialog reflows its own content every frame.
  • The exit animation needs the element to still exist; wait for animationend before calling close(), and skip the wait entirely under reduced motion or the dialog appears to hang.
  • Dialog text sits over an unpredictable page. Keep it at full opacity on a tint of at least 16% or contrast is a lottery.
  • Tailwind's backdrop: variant and its backdrop-blur-* utilities are different things that read similarly — backdrop:backdrop-blur-[14px] is correct and not a typo.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Native dialog with ::backdrop is broadly supported (Chrome 37+, Safari 15.4+, Firefox 98+); Tailwind v4's sheet sets the higher floor. Where backdrop-filter on ::backdrop is unsupported the scrim degrades to a plain dark overlay and the dialog behaves identically.

Techniques used in this demo

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…