Glassmorphism Toast / Notification in React: Frosted Alert Design
Build frosted-glass toast notifications in React with backdrop-filter blur, Tailwind, and smooth animations. Includes copy-paste components and accessibility tips.
Why Glassmorphism Toasts Hit Different
Solid-colored toast notifications are boring. They sit in your corner like a sticky note on a monitor — functional, forgettable. Glassmorphism changes the equation entirely. A frosted-glass notification floats *above* the page content without visually shouting, and when your app already uses a vibrant gradient or a rich background image, the blur effect ties the alert into the design instead of fighting it.
The pattern exploded after macOS Big Sur landed in 2020, and by 2024 you'd see it everywhere — system notifications on iOS, status toasts in Figma's UI, overlay alerts in game launchers. It's not a gimmick. The translucency communicates "temporary" and "non-blocking" better than an opaque box ever could. That's the psychology working for you.
Honestly, most toast libraries give you a className prop and then immediately limit what you can actually style. Building your own frosted toast from scratch takes maybe 40 lines of React. You get full control over blur intensity, border opacity, enter/exit animation, and the stacking behavior — none of which are defaults you'd want to keep anyway. Let's do it right.
The CSS Foundation: backdrop-filter Is Doing All the Work
Before touching React, you need to understand exactly which CSS properties create the effect. Three declarations are mandatory. backdrop-filter: blur(12px) blurs everything *behind* the element — not inside it. background: rgba(255, 255, 255, 0.12) controls translucency; 0.12 is a good starting point for dark backgrounds, bump it to 0.18 for light ones. border: 1px solid rgba(255, 255, 255, 0.25) draws the glass edge highlight that makes the whole thing readable.
Worth noting: backdrop-filter requires the element to have a stacking context, so either position: fixed (which toast containers already use) or will-change: transform will trigger it correctly. You won't hit issues 99% of the time, but if your blur suddenly disappears, that's the first thing to check.
/* Minimal glass toast — pure CSS */
.glass-toast {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); /* Safari */
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
color: #fff;
padding: 14px 18px;
}That inset 0 1px 0 rgba(255,255,255,0.3) on the box-shadow? That's the secret sauce. It adds a top highlight that really sells the convex glass shape — the top edge catches more light. Subtract it and the card looks flat. Keep it and it looks like glass.
In practice, you'll also want a colorful background behind any demo or production page. A plain white or gray page makes the effect invisible. If your app already has gradients, you're set. If not, the glassmorphism generator lets you dial in background colors and preview blur values live before you write a single line.
Building the GlassToast Component in React
Here's a self-contained GlassToast component. No external toast library required — just React state, CSS transitions, and a useEffect for the auto-dismiss timer.
// GlassToast.tsx
import { useEffect, useState } from 'react';
type ToastType = 'success' | 'error' | 'info' | 'warning';
interface GlassToastProps {
message: string;
type?: ToastType;
duration?: number; // ms, default 4000
onClose: () => void;
}
const icons: Record<ToastType, string> = {
success: '✓',
error: '✕',
info: 'ℹ',
warning: '⚠',
};
const accents: Record<ToastType, string> = {
success: 'rgba(74, 222, 128, 0.35)',
error: 'rgba(248, 113, 113, 0.35)',
info: 'rgba(96, 165, 250, 0.35)',
warning: 'rgba(251, 191, 36, 0.35)',
};
export function GlassToast({
message,
type = 'info',
duration = 4000,
onClose,
}: GlassToastProps) {
const [visible, setVisible] = useState(false);
// Trigger enter animation on mount
useEffect(() => {
requestAnimationFrame(() => setVisible(true));
}, []);
// Auto-dismiss
useEffect(() => {
const timer = setTimeout(() => {
setVisible(false);
setTimeout(onClose, 300); // wait for exit transition
}, duration);
return () => clearTimeout(timer);
}, [duration, onClose]);
return (
<div
style={{
background: `rgba(255, 255, 255, 0.10)`,
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
border: `1px solid ${accents[type]}`,
borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2)',
padding: '14px 18px',
display: 'flex',
alignItems: 'center',
gap: '10px',
color: '#fff',
minWidth: '280px',
maxWidth: '420px',
transform: visible ? 'translateY(0)' : 'translateY(16px)',
opacity: visible ? 1 : 0,
transition: 'transform 0.28s ease, opacity 0.28s ease',
cursor: 'pointer',
}}
role="alert"
aria-live="polite"
onClick={() => {
setVisible(false);
setTimeout(onClose, 300);
}}
>
<span style={{ fontSize: '18px', lineHeight: 1 }}>{icons[type]}</span>
<span style={{ fontSize: '14px', fontWeight: 500 }}>{message}</span>
</div>
);
}The translateY(16px) → translateY(0) transition on mount is subtle but it matters — toasts that just pop in feel abrupt. The 16 px slide-up reads as the notification arriving from below, which matches how iOS and Android system toasts behave. Muscle memory works in your favor.
Quick aside: the onClose callback fires *after* the 300 ms exit transition so you don't unmount the element while it's still animating out. If you skip that delay you'll see a hard cut instead of a fade. Not pretty.
Color-coded borders by toast type (success = green tint, error = red tint) give you semantic signaling without relying purely on color — the text and icon also carry the meaning, which matters for accessibility. WCAG 2.2 explicitly says you can't use color as the *only* differentiator.
Toast Container: Stacking Multiple Notifications
One toast is easy. The interesting problem is stacking five of them when your API is having a bad day. You need a container that manages a list, handles addition and removal, and stacks the toasts with proper spacing.
// useToast.tsx — simple toast state manager
import { useState, useCallback, createContext, useContext, ReactNode } from 'react';
import { GlassToast } from './GlassToast';
type ToastType = 'success' | 'error' | 'info' | 'warning';
interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextValue {
addToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((message: string, type: ToastType = 'info') => {
const id = crypto.randomUUID();
setToasts((prev) => [...prev, { id, message, type }]);
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
{/* Fixed container — bottom-right */}
<div
style={{
position: 'fixed',
bottom: '24px',
right: '24px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
zIndex: 9999,
}}
aria-label="Notifications"
>
{toasts.map((t) => (
<GlassToast
key={t.id}
message={t.message}
type={t.type}
onClose={() => removeToast(t.id)}
/>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
const ctx = useContext(ToastContext);
if (!ctx) throw new Error('useToast must be used inside <ToastProvider>');
return ctx;
}Wrap <ToastProvider> around your app root in layout.tsx or _app.tsx and then call useToast() anywhere. The API is intentionally dead simple — one function, two params. You can always add toast queuing, max-count limits, or position variants once the basic version is working.
Look, there's a real temptation to add framer-motion for the stacking animation. It *does* look better with proper AnimatePresence-driven exits. But if you're not already shipping Framer Motion, don't add 140 kB just for toast animations. The CSS transition approach above is genuinely good enough for 95% of apps.
That said, if Framer Motion is already in your bundle — go for it. Replace the inline transform/opacity state logic with <motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }}> inside an <AnimatePresence> block and it'll feel premium.
Tailwind Version: Fewer Lines, Same Result
If your project is already on Tailwind CSS v3+, you can skip the inline styles entirely. Tailwind's backdrop-blur-* utilities and the slash-opacity syntax (bg-white/10) make this almost embarrassingly short.
// GlassToastTailwind.tsx
export function GlassToastTailwind({ message, type = 'info' }) {
const borderColor = {
success: 'border-green-400/40',
error: 'border-red-400/40',
info: 'border-blue-400/40',
warning: 'border-yellow-400/40',
}[type];
return (
<div
className={[
'bg-white/10 backdrop-blur-md',
'border rounded-xl',
borderColor,
'shadow-[0_8px_32px_rgba(0,0,0,0.25),inset_0_1px_0_rgba(255,255,255,0.2)]',
'px-4 py-3 flex items-center gap-2.5',
'text-white text-sm font-medium',
'min-w-[280px] max-w-[420px]',
'cursor-pointer select-none',
'transition-all duration-300',
].join(' ')}
role="alert"
aria-live="polite"
>
{message}
</div>
);
}The backdrop-blur-md utility maps to blur(12px), which is the sweet spot. Go lower (backdrop-blur-sm = 4px) and the glass reads as dirty rather than frosted. Go higher (backdrop-blur-xl = 24px) and you lose all visual connection to what's behind the card — it looks like a tinted solid. 12px is the right number for a notification-sized element.
One more thing — you'll want -webkit-backdrop-filter for Safari. Tailwind doesn't add the prefix automatically unless you're using the @tailwindcss/postcss plugin with autoprefixer. Double-check your PostCSS config if toasts look solid on Safari and blurred on Chrome. That mismatch bites people constantly.
For the background you're displaying these over, the glassmorphism components section of Empire UI includes ready-made gradient backgrounds specifically designed to make blur effects pop. Worth grabbing one rather than reinventing the gradient wheel.
Accessibility: Don't Let the Aesthetic Break Screen Readers
Frosted glass is a visual metaphor. Screen readers don't see the blur. So your semantic markup has to carry all the meaning the visual design implies.
The two attributes you need on every toast: role="alert" and aria-live="polite". The first tells assistive tech this is an important status message. The second announces the content when it's added to the DOM without interrupting whatever the user is currently hearing. Use aria-live="assertive" only for genuine errors — overusing it trains users to ignore it.
// Accessible toast pattern
<div
role="alert"
aria-live={type === 'error' ? 'assertive' : 'polite'}
aria-atomic="true" // read the whole message, not just the changed part
>
<span aria-hidden="true">{icons[type]}</span> {/* decorative icon */}
<span>{message}</span>
</div>Contrast is the other landmine. White text on rgba(255,255,255,0.10) over a purple gradient might pass WCAG AA on paper but fail in practice when a lighter part of the gradient sits behind the toast. Test with actual WCAG contrast checking against the *lightest possible background value* that can appear, not just the average. Adding text-shadow: 0 1px 2px rgba(0,0,0,0.5) on the text buys you a lot of legibility headroom without changing the aesthetic.
Finally: prefers-reduced-motion. The enter/exit CSS transitions should be conditional. Wrap them in @media (prefers-reduced-motion: no-preference) or use the React hook pattern with window.matchMedia('(prefers-reduced-motion: reduce)'). Users who've explicitly asked for less motion on their OS shouldn't get slide-in animations every time your API returns a 200.
Integrating with react-hot-toast and Existing Libraries
You might already be using react-hot-toast or sonner and don't want to rip them out. Good news — both support custom renderers, so you can inject the glassmorphism styling without replacing the library's queue management.
// Custom renderer for react-hot-toast
import toast, { Toaster, ToastBar } from 'react-hot-toast';
export function GlassToaster() {
return (
<Toaster
position="bottom-right"
toastOptions={{ duration: 4000 }}
>
{(t) => (
<ToastBar toast={t}>
{({ icon, message }) => (
<div
style={{
background: 'rgba(255,255,255,0.10)',
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
border: '1px solid rgba(255,255,255,0.20)',
borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0,0,0,0.25)',
padding: '12px 16px',
display: 'flex',
alignItems: 'center',
gap: '8px',
color: '#fff',
fontSize: '14px',
}}
onClick={() => toast.dismiss(t.id)}
>
{icon}
{message}
</div>
)}
</ToastBar>
)}
</Toaster>
);
}This pattern works with sonner too — it exposes a cn and classNames API on <Toaster> where you can inject the glass styles. Check their docs for the exact prop names; they changed between v1 and v2.
In practice, if you're starting a new project, the custom hook approach from the previous section is simpler than fighting a third-party library's styling model. But for existing apps where react-hot-toast is already managing toast queuing, deduplication, and keyboard interactions — the custom renderer is the pragmatic path.
Whatever approach you pick, pair it with a proper gradient background. The gradient generator tool lets you create and export CSS gradient strings in about 30 seconds — much faster than fiddling with HSL values in DevTools.
FAQ
Yes, as of 2026 Chrome, Firefox, and Safari all support backdrop-filter with no flags. Add -webkit-backdrop-filter for older Safari versions and you're covered.
Both libraries support custom toast renderers. Pass your glass-styled div through their render callback and the library handles queue management while you control the look.
backdrop-filter: blur(12px) to blur(14px) is the sweet spot for small UI elements. Lower and it looks dirty; higher and you lose the visual connection to the background.
Add role="alert" and aria-live="polite" (or "assertive" for errors) to the toast element. Mark decorative icons with aria-hidden="true" so they're not read aloud.
