Glass Toast
Original · freeglassmorphic toasts
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/toast-glass.json"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import type { FocusEvent, KeyboardEvent, ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type ToastVariant = "success" | "error" | "info" | "warning";
type ToastPosition = "top" | "bottom";
interface ToastItem {
id: number;
variant: ToastVariant;
title: string;
description: string;
action?: string;
}
const MAX_STACK = 4;
const DURATIONS: Record<ToastVariant, number> = {
success: 5000,
info: 5500,
warning: 7000,
error: 8000,
};
const MESSAGES: Record<
ToastVariant,
ReadonlyArray<{ title: string; description: string; action?: string }>
> = {
success: [
{
title: "Deployment is live",
description: "webinnoventix.com is now serving from 14 edge regions.",
action: "View build",
},
{
title: "Payment received",
description: "Invoice INV-2043 for $480 was settled by Northwind Studio.",
action: "Open receipt",
},
],
error: [
{
title: "Upload failed",
description: "hero-v3.mp4 is 118 MB — over the 100 MB Starter limit.",
action: "Retry",
},
{
title: "Card was declined",
description: "The card ending 4291 was rejected. Update billing to keep Pro.",
action: "Fix billing",
},
],
info: [
{
title: "New comment from Priya",
description: "She left a note on line 24 of the Q3 roadmap draft.",
action: "Open thread",
},
{
title: "Import complete",
description: "1,204 rows synced from the Notion project database.",
},
],
warning: [
{
title: "Storage almost full",
description: "You've used 92% of your 50 GB plan. Archive old exports soon.",
action: "Manage files",
},
{
title: "Session expiring",
description: "You'll be signed out in 5 minutes unless you stay active.",
action: "Stay signed in",
},
],
};
const VARIANT_STYLE: Record<
ToastVariant,
{ role: "status" | "alert"; bar: string; chip: string; icon: string; dot: string }
> = {
success: {
role: "status",
bar: "bg-emerald-500",
chip: "bg-emerald-500/15 ring-emerald-500/30",
icon: "text-emerald-600 dark:text-emerald-400",
dot: "bg-emerald-500",
},
error: {
role: "alert",
bar: "bg-rose-500",
chip: "bg-rose-500/15 ring-rose-500/30",
icon: "text-rose-600 dark:text-rose-400",
dot: "bg-rose-500",
},
info: {
role: "status",
bar: "bg-sky-500",
chip: "bg-sky-500/15 ring-sky-500/30",
icon: "text-sky-600 dark:text-sky-400",
dot: "bg-sky-500",
},
warning: {
role: "alert",
bar: "bg-amber-500",
chip: "bg-amber-500/15 ring-amber-500/30",
icon: "text-amber-600 dark:text-amber-400",
dot: "bg-amber-500",
},
};
const LABELS: Record<ToastVariant, string> = {
success: "Success",
error: "Error",
info: "Info",
warning: "Warning",
};
function IconCheck({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M20 6 9 17l-5-5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconAlert({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M12 8.5v5m0 3.5h.01M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.7 3.86a2 2 0 0 0-3.42 0Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconInfo({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<circle cx="12" cy="12" r="9.25" stroke="currentColor" strokeWidth="2" />
<path
d="M12 11v5m0-8.5h.01"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconXCircle({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<circle cx="12" cy="12" r="9.25" stroke="currentColor" strokeWidth="2" />
<path
d="m15 9-6 6m0-6 6 6"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconClose({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="m6 6 12 12M18 6 6 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
const ICONS: Record<ToastVariant, (p: { className?: string }) => ReactNode> = {
success: IconCheck,
error: IconXCircle,
info: IconInfo,
warning: IconAlert,
};
function ToastCard({
toast,
reduced,
onDismiss,
}: {
toast: ToastItem;
reduced: boolean;
onDismiss: (id: number) => void;
}) {
const style = VARIANT_STYLE[toast.variant];
const Icon = ICONS[toast.variant];
const duration = DURATIONS[toast.variant];
const barRef = useRef<HTMLDivElement>(null);
const elapsedRef = useRef(0);
const pausedRef = useRef(false);
const hoverRef = useRef(false);
const focusRef = useRef(false);
const [paused, setPaused] = useState(false);
const sync = useCallback(() => {
const next = hoverRef.current || focusRef.current;
pausedRef.current = next;
setPaused(next);
}, []);
useEffect(() => {
let raf = 0;
let last = performance.now();
const tick = (now: number) => {
const dt = now - last;
last = now;
if (!pausedRef.current) elapsedRef.current += dt;
const remain = Math.max(0, duration - elapsedRef.current);
if (barRef.current) {
barRef.current.style.width = `${(remain / duration) * 100}%`;
}
if (remain <= 0) {
onDismiss(toast.id);
return;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [toast.id, duration, onDismiss]);
const handleFocus = () => {
focusRef.current = true;
sync();
};
const handleBlur = (e: FocusEvent<HTMLDivElement>) => {
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
focusRef.current = false;
sync();
}
};
const handleEnter = () => {
hoverRef.current = true;
sync();
};
const handleLeave = () => {
hoverRef.current = false;
sync();
};
const handleKey = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
e.preventDefault();
onDismiss(toast.id);
}
};
const enter = reduced
? { opacity: 0 }
: { opacity: 0, x: 44, scale: 0.94, filter: "blur(6px)" };
const shown = reduced
? { opacity: 1 }
: { opacity: 1, x: 0, scale: 1, filter: "blur(0px)" };
const leave = reduced
? { opacity: 0 }
: { opacity: 0, x: 44, scale: 0.94, filter: "blur(6px)" };
const transition = reduced
? { duration: 0.15 }
: ({ type: "spring", stiffness: 460, damping: 34, mass: 0.85 } as const);
return (
<motion.li
layout={!reduced}
initial={enter}
animate={shown}
exit={leave}
transition={transition}
className="pointer-events-auto list-none"
>
<div
role={style.role}
aria-atomic="true"
tabIndex={0}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKey}
className="tg-card group relative overflow-hidden rounded-2xl border border-white/60 bg-white/55 shadow-[0_18px_50px_-18px_rgba(15,23,42,0.5)] ring-1 ring-black/5 backdrop-blur-xl transition-shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white/40 dark:border-white/10 dark:bg-slate-900/55 dark:ring-white/10 dark:focus-visible:ring-offset-slate-900/40"
>
<span aria-hidden="true" className="tg-sheen" />
<div className="flex items-start gap-3 p-4">
<span
className={`grid h-9 w-9 shrink-0 place-items-center rounded-xl ring-1 ${style.chip} ${style.icon}`}
>
<Icon className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-50">
{toast.title}
</p>
<p className="mt-0.5 text-sm leading-snug text-slate-600 dark:text-slate-300">
{toast.description}
</p>
{toast.action ? (
<div className="mt-2.5">
<button
type="button"
onClick={() => onDismiss(toast.id)}
className="rounded-lg px-2.5 py-1 text-xs font-semibold text-slate-700 ring-1 ring-inset ring-black/10 transition-colors hover:bg-black/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-100 dark:ring-white/15 dark:hover:bg-white/10"
>
{toast.action}
</button>
</div>
) : null}
</div>
<button
type="button"
aria-label={`Dismiss notification: ${toast.title}`}
onClick={() => onDismiss(toast.id)}
className="-mr-1 -mt-1 shrink-0 rounded-lg p-1.5 text-slate-500 transition-colors hover:bg-black/[0.05] hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-slate-50"
>
<IconClose className="h-4 w-4" />
</button>
</div>
<div className="h-1 w-full bg-black/5 dark:bg-white/10">
<div
ref={barRef}
className={`h-full ${style.bar}`}
style={{ width: "100%" }}
/>
</div>
<span className="sr-only" aria-live="off">
{paused ? "Timer paused" : ""}
</span>
</div>
</motion.li>
);
}
function PositionToggle({
value,
onChange,
}: {
value: ToastPosition;
onChange: (p: ToastPosition) => void;
}) {
const options: ReadonlyArray<{ id: ToastPosition; label: string }> = [
{ id: "top", label: "Top" },
{ id: "bottom", label: "Bottom" },
];
const refs = useRef<Record<ToastPosition, HTMLButtonElement | null>>({
top: null,
bottom: null,
});
const move = (next: ToastPosition) => {
onChange(next);
refs.current[next]?.focus();
};
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (
e.key === "ArrowRight" ||
e.key === "ArrowLeft" ||
e.key === "ArrowUp" ||
e.key === "ArrowDown"
) {
e.preventDefault();
move(value === "top" ? "bottom" : "top");
}
};
return (
<div
role="radiogroup"
aria-label="Toast position"
onKeyDown={onKeyDown}
className="inline-flex rounded-xl border border-slate-300/70 bg-white/60 p-1 backdrop-blur dark:border-white/10 dark:bg-slate-900/50"
>
{options.map((o) => {
const active = value === o.id;
return (
<button
key={o.id}
ref={(el) => {
refs.current[o.id] = el;
}}
type="button"
role="radio"
aria-checked={active}
tabIndex={active ? 0 : -1}
onClick={() => onChange(o.id)}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
active
? "bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-900"
: "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white"
}`}
>
{o.label}
</button>
);
})}
</div>
);
}
export default function ToastGlass() {
const reduced = !!useReducedMotion();
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [position, setPosition] = useState<ToastPosition>("bottom");
const idRef = useRef(1);
const dismiss = useCallback((id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const fire = useCallback((variant: ToastVariant) => {
const pool = MESSAGES[variant];
const pick = pool[Math.floor(Math.random() * pool.length)];
const id = idRef.current++;
setToasts((prev) => {
const next = [...prev, { id, variant, ...pick }];
return next.slice(-MAX_STACK);
});
}, []);
const clearAll = useCallback(() => setToasts([]), []);
const triggers: ReadonlyArray<ToastVariant> = [
"success",
"error",
"info",
"warning",
];
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-100 via-white to-slate-100 px-6 py-20 text-slate-900 sm:py-24 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 dark:text-slate-100">
<style>{`
@keyframes tg_float {
0%, 100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(0, -30px, 0); }
}
@keyframes tg_drift {
0%, 100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(26px, 18px, 0); }
}
@keyframes tg_sheen {
0% { background-position: -220% 0; }
100% { background-position: 220% 0; }
}
.tg-b1 { animation: tg_float 9s ease-in-out infinite; }
.tg-b2 { animation: tg_drift 11s ease-in-out infinite; }
.tg-b3 { animation: tg_float 13s ease-in-out infinite; }
.tg-sheen {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.95), transparent);
background-size: 220% 100%;
animation: tg_sheen 3.6s linear infinite;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.tg-b1, .tg-b2, .tg-b3, .tg-sheen { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden"
>
<div className="tg-b1 absolute -left-16 -top-24 h-72 w-72 rounded-full bg-indigo-400/30 blur-3xl dark:bg-indigo-500/20" />
<div className="tg-b2 absolute -right-20 top-1/3 h-80 w-80 rounded-full bg-violet-400/25 blur-3xl dark:bg-violet-600/20" />
<div className="tg-b3 absolute -bottom-24 left-1/4 h-72 w-72 rounded-full bg-sky-300/25 blur-3xl dark:bg-sky-500/15" />
</div>
<div className="relative mx-auto flex min-h-[560px] w-full max-w-3xl flex-col items-center text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/60 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 backdrop-blur dark:border-white/10 dark:bg-slate-900/50 dark:text-indigo-300">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Notifications
</span>
<h2 className="mt-6 text-4xl font-semibold tracking-tight sm:text-5xl">
Glassmorphic toasts
</h2>
<p className="mt-4 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-300">
Frosted, stackable alerts that pause when you hover or focus them,
dismiss with a click or the Escape key, and announce themselves to
screen readers as they arrive.
</p>
<div className="mt-9 flex flex-wrap items-center justify-center gap-2.5">
{triggers.map((v) => {
const s = VARIANT_STYLE[v];
return (
<button
key={v}
type="button"
onClick={() => fire(v)}
className="group inline-flex items-center gap-2 rounded-xl border border-slate-300/70 bg-white/70 px-4 py-2.5 text-sm font-semibold text-slate-800 shadow-sm backdrop-blur transition-all hover:-translate-y-0.5 hover:bg-white hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 active:translate-y-0 dark:border-white/10 dark:bg-slate-900/60 dark:text-slate-100 dark:hover:bg-slate-800/80"
>
<span className={`h-2 w-2 rounded-full ${s.dot}`} />
{LABELS[v]}
</button>
);
})}
</div>
<div className="mt-6 flex flex-wrap items-center justify-center gap-4">
<PositionToggle value={position} onChange={setPosition} />
<button
type="button"
onClick={clearAll}
disabled={toasts.length === 0}
className="rounded-xl px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 dark:text-slate-300 dark:hover:text-white"
>
Clear all{toasts.length ? ` (${toasts.length})` : ""}
</button>
</div>
<p className="mt-6 text-xs text-slate-500 dark:text-slate-400">
Hover to pause the timer · Focus a toast and press Esc to close ·
Arrow keys switch the corner
</p>
</div>
<ul
aria-label="Notifications"
className={`pointer-events-none absolute z-10 flex w-[min(92vw,22rem)] gap-3 ${
position === "top"
? "right-4 top-4 flex-col-reverse sm:right-8 sm:top-8"
: "bottom-4 right-4 flex-col sm:bottom-8 sm:right-8"
}`}
>
<AnimatePresence mode="popLayout" initial={false}>
{toasts.map((toast) => (
<ToastCard
key={toast.id}
toast={toast}
reduced={reduced}
onDismiss={dismiss}
/>
))}
</AnimatePresence>
</ul>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Toast
Originalbasic toast notifications

Variants Toast
Originalsuccess/info/warning/error toasts
Icon Toast
Originaltoasts with icon and title

Progress Toast
Originaltoast with auto-dismiss progress

Action Toast
Originaltoast with an undo action

Stacked Toast
Originalstacked toast queue

Slide In Toast
Originalslide-in corner toasts

Promise Toast
Originalloading-to-result promise toast

Position Toast
Originaltoast position picker demo

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

