05 / 20
All 20 in 20 Tailwind Liquid Glass Effects
- 01Tailwind Liquid Glass Card with Hover Refraction
- 02Tailwind Liquid Glass Button with Glossy Highlight
- 03Tailwind Liquid Glass Navigation Bar
- 04Tailwind Liquid Gooey Toggle Switch
- 05Tailwind Liquid Glass Modal Dialog
- 06Tailwind Liquid Glass Theme Switcher
- 07Tailwind Liquid Glass Input Form Fields
- 08Tailwind SVG Displacement Liquid Glass Background
- 09Tailwind Liquid Glass Dropdown Menu
- 10Tailwind Liquid Glass Pricing Table
- 11Tailwind Liquid Glass Tooltip Popover
- 12Tailwind Liquid Glass Audio Player UI
- 13Tailwind Liquid Glass Range Slider
- 14Tailwind Liquid Glass Toast Notification
- 15Tailwind Liquid Glass Sidebar Navigation
- 16Tailwind Liquid Glass Accordion
- 17Tailwind Liquid Glass Login Card
- 18Tailwind Liquid Glass Floating Action Button
- 19Tailwind Liquid Glass Loading Skeleton
- 20Tailwind Liquid Glass Multi-Step Form Wizard
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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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><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;
}
}@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(); });
})();(() => {
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.
<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-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;
}
}@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;
}
}<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">
<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::backdroppseudo-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 adata-closingattribute styled bydata-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
animationendwait entirely — withanimate-nonethe event never fires, and a dialog that will not close is worse than one that does not animate.
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 — 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(); });
})();
```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 — 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, orbackdrop:backdrop-grayscalefor a stark, editorial modal. - Restyle the entrance by editing the
@keyframesbehind--animate-lgt05-in; a slightrotate(-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-accenttheme entry. - Add a close X in the corner as a real
<button>with anaria-label— icon-only controls still need names.
Gotchas — read before shipping
- Use
<dialog>andshowModal(). 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. ::backdroponly exists for dialogs opened withshowModal(). Callingshow()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
animationendbefore callingclose(), 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 itsbackdrop-blur-*utilities are different things that read similarly —backdrop:backdrop-blur-[14px]is correct and not a typo.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.