14 / 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 Toast Notification
Frosted toasts sliding in from the top-right and stacking without turning to mush. The port's real content is the anti-pattern it avoids: each toast gets isolate so overlapping frosts never compound, and the type variants are data-type attributes styled by variants rather than three near-identical class strings in the script.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section id="lgt-14-root" class="lgt-14 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt14-bg font-lgt14-sans" aria-label="Tailwind liquid glass toast notifications">
<img src="https://picsum.photos/seed/lgtoast/1100/750" width="1100" height="750" alt="Dashboard backdrop" class="absolute inset-0 size-full object-cover">
<div class="relative z-1 flex flex-wrap justify-center gap-3">
<button type="button" data-spawn="success" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Success toast</button>
<button type="button" data-spawn="info" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Info toast</button>
<button type="button" data-spawn="error" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Error toast</button>
</div>
<div id="lgt-14-region" role="region" aria-label="Notifications" aria-live="polite" class="pointer-events-none absolute top-5 right-5 z-50 flex w-[min(320px,86vw)] flex-col gap-3"></div>
<template id="lgt-14-tpl">
<div class="pointer-events-auto relative isolate flex items-start gap-3 overflow-hidden rounded-[16px] border border-white/30 bg-white/14 py-3.5 pr-10 pl-3.5 text-white backdrop-blur-[20px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_20px_40px_-20px_rgba(0,0,0,.7)] animate-lgt14-in data-closing:animate-lgt14-out motion-reduce:animate-none after:absolute after:bottom-0 after:left-0 after:h-0.75 after:w-full after:origin-left after:bg-white/70 after:[animation:lgt-14-rim_var(--life,4000ms)_linear_forwards] motion-reduce:after:hidden">
<span data-slot="ico" class="grid size-6.5 flex-none place-items-center rounded-full text-sm font-black text-white data-[type=success]:bg-lgt14-ok data-[type=info]:bg-lgt14-info data-[type=error]:bg-lgt14-err"></span>
<div class="text-sm leading-snug"><strong data-slot="title" class="mb-0.5 block font-extrabold"></strong><span data-slot="body"></span></div>
<button type="button" data-slot="close" aria-label="Dismiss" class="absolute top-2 right-2 grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-white/16 text-[13px] text-white hover:bg-white/30 focus-visible:bg-white/30 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-white">✕</button>
</div>
</template>
</section><section id="lgt-14-root" class="lgt-14 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt14-bg font-lgt14-sans" aria-label="Tailwind liquid glass toast notifications">
<img src="https://picsum.photos/seed/lgtoast/1100/750" width="1100" height="750" alt="Dashboard backdrop" class="absolute inset-0 size-full object-cover">
<div class="relative z-1 flex flex-wrap justify-center gap-3">
<button type="button" data-spawn="success" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Success toast</button>
<button type="button" data-spawn="info" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Info toast</button>
<button type="button" data-spawn="error" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Error toast</button>
</div>
<div id="lgt-14-region" role="region" aria-label="Notifications" aria-live="polite" class="pointer-events-none absolute top-5 right-5 z-50 flex w-[min(320px,86vw)] flex-col gap-3"></div>
<template id="lgt-14-tpl">
<div class="pointer-events-auto relative isolate flex items-start gap-3 overflow-hidden rounded-[16px] border border-white/30 bg-white/14 py-3.5 pr-10 pl-3.5 text-white backdrop-blur-[20px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_20px_40px_-20px_rgba(0,0,0,.7)] animate-lgt14-in data-closing:animate-lgt14-out motion-reduce:animate-none after:absolute after:bottom-0 after:left-0 after:h-0.75 after:w-full after:origin-left after:bg-white/70 after:[animation:lgt-14-rim_var(--life,4000ms)_linear_forwards] motion-reduce:after:hidden">
<span data-slot="ico" class="grid size-6.5 flex-none place-items-center rounded-full text-sm font-black text-white data-[type=success]:bg-lgt14-ok data-[type=info]:bg-lgt14-info data-[type=error]:bg-lgt14-err"></span>
<div class="text-sm leading-snug"><strong data-slot="title" class="mb-0.5 block font-extrabold"></strong><span data-slot="body"></span></div>
<button type="button" data-slot="close" aria-label="Dismiss" class="absolute top-2 right-2 grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-white/16 text-[13px] text-white hover:bg-white/30 focus-visible:bg-white/30 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-white">✕</button>
</div>
</template>
</section>@import "tailwindcss";
@theme {
--color-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}@import "tailwindcss";
@theme {
--color-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}(() => {
const root = document.querySelector('#lgt-14-root');
const region = root.querySelector('#lgt-14-region');
const tpl = root.querySelector('#lgt-14-tpl');
const LIFE = 4000;
const copy = {
success: { ico: '✓', title: 'Saved', body: 'Your changes are live.' },
info: { ico: 'i', title: 'Heads up', body: 'A new version is available.' },
error: { ico: '!', title: 'Upload failed', body: 'Check your connection and retry.' }
};
const toast = type => {
const c = copy[type] || copy.info;
const el = tpl.content.firstElementChild.cloneNode(true);
el.style.setProperty('--life', LIFE + 'ms');
const ico = el.querySelector('[data-slot="ico"]');
ico.dataset.type = type;
ico.textContent = c.ico;
el.querySelector('[data-slot="title"]').textContent = c.title;
el.querySelector('[data-slot="body"]').textContent = c.body;
region.appendChild(el);
const kill = () => {
el.dataset.closing = '';
el.addEventListener('animationend', () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400);
};
const t = setTimeout(kill, LIFE);
el.querySelector('[data-slot="close"]').addEventListener('click', () => { clearTimeout(t); kill(); });
};
root.querySelectorAll('[data-spawn]').forEach(b => b.addEventListener('click', () => toast(b.dataset.spawn)));
})();(() => {
const root = document.querySelector('#lgt-14-root');
const region = root.querySelector('#lgt-14-region');
const tpl = root.querySelector('#lgt-14-tpl');
const LIFE = 4000;
const copy = {
success: { ico: '✓', title: 'Saved', body: 'Your changes are live.' },
info: { ico: 'i', title: 'Heads up', body: 'A new version is available.' },
error: { ico: '!', title: 'Upload failed', body: 'Check your connection and retry.' }
};
const toast = type => {
const c = copy[type] || copy.info;
const el = tpl.content.firstElementChild.cloneNode(true);
el.style.setProperty('--life', LIFE + 'ms');
const ico = el.querySelector('[data-slot="ico"]');
ico.dataset.type = type;
ico.textContent = c.ico;
el.querySelector('[data-slot="title"]').textContent = c.title;
el.querySelector('[data-slot="body"]').textContent = c.body;
region.appendChild(el);
const kill = () => {
el.dataset.closing = '';
el.addEventListener('animationend', () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400);
};
const t = setTimeout(kill, LIFE);
el.querySelector('[data-slot="close"]').addEventListener('click', () => { clearTimeout(t); kill(); });
};
root.querySelectorAll('[data-spawn]').forEach(b => b.addEventListener('click', () => toast(b.dataset.spawn)));
})();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-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}@import "tailwindcss";
@theme {
--color-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(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.
isolateper toast is the whole fix for compounding backdrop blur — one utility replacing an entire category of glass bug.- Toast markup lives in a
<template>with real Tailwind classes, so the compiler sees every utility. Building class strings in JS would leave them uncompiled. - Type is a
data-typeattribute styled bydata-[type=...]:variants; the script sets meaning, the markup owns appearance. - The depleting rim is an
after:layer animatingscaleX, so a stack of counting-down toasts costs no layout. - Entry, exit and rim are three
--animate-*theme entries, with the exit driven by adata-closingattribute rather than a styling class.
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 Toast Notification
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-glass-toast-notification/
Frosted toasts sliding in from the top-right and stacking without turning to mush. The port's real content is the anti-pattern it avoids: each toast gets isolate so overlapping frosts never compound, and the type variants are data-type attributes styled by variants rather than three near-identical class strings in the script.
## HTML
```html
<section id="lgt-14-root" class="lgt-14 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt14-bg font-lgt14-sans" aria-label="Tailwind liquid glass toast notifications">
<img src="https://picsum.photos/seed/lgtoast/1100/750" width="1100" height="750" alt="Dashboard backdrop" class="absolute inset-0 size-full object-cover">
<div class="relative z-1 flex flex-wrap justify-center gap-3">
<button type="button" data-spawn="success" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Success toast</button>
<button type="button" data-spawn="info" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Info toast</button>
<button type="button" data-spawn="error" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Error toast</button>
</div>
<div id="lgt-14-region" role="region" aria-label="Notifications" aria-live="polite" class="pointer-events-none absolute top-5 right-5 z-50 flex w-[min(320px,86vw)] flex-col gap-3"></div>
<template id="lgt-14-tpl">
<div class="pointer-events-auto relative isolate flex items-start gap-3 overflow-hidden rounded-[16px] border border-white/30 bg-white/14 py-3.5 pr-10 pl-3.5 text-white backdrop-blur-[20px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_20px_40px_-20px_rgba(0,0,0,.7)] animate-lgt14-in data-closing:animate-lgt14-out motion-reduce:animate-none after:absolute after:bottom-0 after:left-0 after:h-0.75 after:w-full after:origin-left after:bg-white/70 after:[animation:lgt-14-rim_var(--life,4000ms)_linear_forwards] motion-reduce:after:hidden">
<span data-slot="ico" class="grid size-6.5 flex-none place-items-center rounded-full text-sm font-black text-white data-[type=success]:bg-lgt14-ok data-[type=info]:bg-lgt14-info data-[type=error]:bg-lgt14-err"></span>
<div class="text-sm leading-snug"><strong data-slot="title" class="mb-0.5 block font-extrabold"></strong><span data-slot="body"></span></div>
<button type="button" data-slot="close" aria-label="Dismiss" class="absolute top-2 right-2 grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-white/16 text-[13px] text-white hover:bg-white/30 focus-visible:bg-white/30 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-white">✕</button>
</div>
</template>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('#lgt-14-root');
const region = root.querySelector('#lgt-14-region');
const tpl = root.querySelector('#lgt-14-tpl');
const LIFE = 4000;
const copy = {
success: { ico: '✓', title: 'Saved', body: 'Your changes are live.' },
info: { ico: 'i', title: 'Heads up', body: 'A new version is available.' },
error: { ico: '!', title: 'Upload failed', body: 'Check your connection and retry.' }
};
const toast = type => {
const c = copy[type] || copy.info;
const el = tpl.content.firstElementChild.cloneNode(true);
el.style.setProperty('--life', LIFE + 'ms');
const ico = el.querySelector('[data-slot="ico"]');
ico.dataset.type = type;
ico.textContent = c.ico;
el.querySelector('[data-slot="title"]').textContent = c.title;
el.querySelector('[data-slot="body"]').textContent = c.body;
region.appendChild(el);
const kill = () => {
el.dataset.closing = '';
el.addEventListener('animationend', () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400);
};
const t = setTimeout(kill, LIFE);
el.querySelector('[data-slot="close"]').addEventListener('click', () => { clearTimeout(t); kill(); });
};
root.querySelectorAll('[data-spawn]').forEach(b => b.addEventListener('click', () => toast(b.dataset.spawn)));
})();
```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 Toast Notification
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-glass-toast-notification/
Frosted toasts sliding in from the top-right and stacking without turning to mush. The port's real content is the anti-pattern it avoids: each toast gets isolate so overlapping frosts never compound, and the type variants are data-type attributes styled by variants rather than three near-identical class strings in the script.
## HTML
```html
<section id="lgt-14-root" class="lgt-14 relative grid w-full min-h-svh place-items-center overflow-hidden bg-lgt14-bg font-lgt14-sans" aria-label="Tailwind liquid glass toast notifications">
<img src="https://picsum.photos/seed/lgtoast/1100/750" width="1100" height="750" alt="Dashboard backdrop" class="absolute inset-0 size-full object-cover">
<div class="relative z-1 flex flex-wrap justify-center gap-3">
<button type="button" data-spawn="success" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Success toast</button>
<button type="button" data-spawn="info" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Info toast</button>
<button type="button" data-spawn="error" class="cursor-pointer rounded-full border border-white/35 bg-white/14 px-5 py-3 text-sm font-bold text-white backdrop-blur-[14px] backdrop-saturate-160 transition-transform duration-150 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">Error toast</button>
</div>
<div id="lgt-14-region" role="region" aria-label="Notifications" aria-live="polite" class="pointer-events-none absolute top-5 right-5 z-50 flex w-[min(320px,86vw)] flex-col gap-3"></div>
<template id="lgt-14-tpl">
<div class="pointer-events-auto relative isolate flex items-start gap-3 overflow-hidden rounded-[16px] border border-white/30 bg-white/14 py-3.5 pr-10 pl-3.5 text-white backdrop-blur-[20px] backdrop-saturate-180 shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_20px_40px_-20px_rgba(0,0,0,.7)] animate-lgt14-in data-closing:animate-lgt14-out motion-reduce:animate-none after:absolute after:bottom-0 after:left-0 after:h-0.75 after:w-full after:origin-left after:bg-white/70 after:[animation:lgt-14-rim_var(--life,4000ms)_linear_forwards] motion-reduce:after:hidden">
<span data-slot="ico" class="grid size-6.5 flex-none place-items-center rounded-full text-sm font-black text-white data-[type=success]:bg-lgt14-ok data-[type=info]:bg-lgt14-info data-[type=error]:bg-lgt14-err"></span>
<div class="text-sm leading-snug"><strong data-slot="title" class="mb-0.5 block font-extrabold"></strong><span data-slot="body"></span></div>
<button type="button" data-slot="close" aria-label="Dismiss" class="absolute top-2 right-2 grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-white/16 text-[13px] text-white hover:bg-white/30 focus-visible:bg-white/30 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-white">✕</button>
</div>
</template>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-lgt14-bg: #0e0b1a;
--color-lgt14-ok: #34c48a;
--color-lgt14-ok: oklch(0.72 0.16 160);
--color-lgt14-info: #5b8def;
--color-lgt14-info: oklch(0.7 0.16 250);
--color-lgt14-err: #e2503c;
--color-lgt14-err: oklch(0.65 0.2 25);
--font-lgt14-sans: 'Manrope', system-ui, sans-serif;
--animate-lgt14-in: lgt-14-in 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
--animate-lgt14-out: lgt-14-out 0.3s ease forwards;
}
@keyframes lgt-14-in {
from {
opacity: 0;
transform: translateX(110%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes lgt-14-out {
to {
opacity: 0;
transform: translateX(110%);
}
}
@keyframes lgt-14-rim {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('#lgt-14-root');
const region = root.querySelector('#lgt-14-region');
const tpl = root.querySelector('#lgt-14-tpl');
const LIFE = 4000;
const copy = {
success: { ico: '✓', title: 'Saved', body: 'Your changes are live.' },
info: { ico: 'i', title: 'Heads up', body: 'A new version is available.' },
error: { ico: '!', title: 'Upload failed', body: 'Check your connection and retry.' }
};
const toast = type => {
const c = copy[type] || copy.info;
const el = tpl.content.firstElementChild.cloneNode(true);
el.style.setProperty('--life', LIFE + 'ms');
const ico = el.querySelector('[data-slot="ico"]');
ico.dataset.type = type;
ico.textContent = c.ico;
el.querySelector('[data-slot="title"]').textContent = c.title;
el.querySelector('[data-slot="body"]').textContent = c.body;
region.appendChild(el);
const kill = () => {
el.dataset.closing = '';
el.addEventListener('animationend', () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400);
};
const t = setTimeout(kill, LIFE);
el.querySelector('[data-slot="close"]').addEventListener('click', () => { clearTimeout(t); kill(); });
};
root.querySelectorAll('[data-spawn]').forEach(b => b.addEventListener('click', () => toast(b.dataset.spawn)));
})();
```How this works
Stacked glass compounds unless you stop it. Two frosted panels overlapping means the upper one samples the lower one's already-blurred output, and by the third toast the stack is grey soup. isolate gives each toast its own stacking context, which is the entire fix and is one utility.
class="relative isolate overflow-hidden
border border-white/30 bg-white/14
backdrop-blur-[20px] backdrop-saturate-180"Type is data, not a class name. The script sets data-type="error"; the markup carries data-[type=error]:bg-lgt14-err on the icon. Adding a fourth type means adding one variant, not editing a JS string that builds class names — which the compiler could not see anyway.
The lifetime rim is a transform. An after: bar scaling from 1 to 0 over --life, matched to the dismiss timer. Because it is scaleX and not width, a row of toasts counting down costs nothing.
The region is polite and the buttons are real. Screen-reader users hear each toast once, and nobody is trapped waiting for an auto-dismiss they cannot cancel.
Make it yours
- Change lifetime in one place: the
LIFEconstant sets both the timer and the--lifethe rim animation reads. - Add a type by adding a
data-[type=...]:variant and a copy entry — no new class string, no new rule block. - Move the region to another corner by editing the fixed offsets and flipping the slide direction in the keyframes.
- Cap the stack at three and collapse older toasts if your app is chatty; the region is a normal flex column.
- Pause the timer on hover with a
[animation-play-state:paused]utility for long-form messages.
Gotchas — read before shipping
- Without
isolate, overlapping frosted toasts sample each other and the stack degrades into mush — the single most common bug in glass notification systems. - Class strings built in JavaScript are invisible to the compiler. Set data attributes and let variants in the markup do the styling.
- Remove toast nodes on exit. An unbounded region of expired, still-composited glass panels is a slow leak that only shows up in long sessions.
aria-live="assertive"interrupts whatever the user is reading. Usepoliteunless the message is genuinely urgent.- Clear the auto-dismiss timer when the user dismisses manually, or a removed node's timer fires into nothing and the next toast inherits confusing timing.
- Under
motion-reducethe entry animation is disabled, so the toast must be visible by default rather than starting atopacity-0.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
backdrop-filter for the frost and plain JavaScript for the queue. Degrades to solid tinted toasts where backdrop-filter is missing; the stacking, timing and announcements are unaffected.