Position Toast
Original · freetoast position picker demo
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-position.json"use client";
import {
useEffect,
useRef,
useState,
type KeyboardEvent,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type ToastType = "success" | "error" | "info" | "warning";
type Position =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right";
interface ToastItem {
id: number;
type: ToastType;
title: string;
description: string;
duration: number;
}
interface TimerRecord {
handle: ReturnType<typeof setTimeout> | undefined;
remaining: number;
startedAt: number;
}
const MAX_TOASTS = 4;
const POSITIONS: Position[] = [
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
];
const TYPES: ToastType[] = ["success", "error", "info", "warning"];
const POSITION_LABEL: Record<Position, string> = {
"top-left": "Top left",
"top-center": "Top center",
"top-right": "Top right",
"bottom-left": "Bottom left",
"bottom-center": "Bottom center",
"bottom-right": "Bottom right",
};
const CELL_ALIGN: Record<Position, string> = {
"top-left": "items-start justify-start",
"top-center": "items-start justify-center",
"top-right": "items-start justify-end",
"bottom-left": "items-end justify-start",
"bottom-center": "items-end justify-center",
"bottom-right": "items-end justify-end",
};
const ANCHOR_CLASS: Record<Position, string> = {
"top-left": "top-3 left-3 items-start",
"top-center": "top-3 left-1/2 -translate-x-1/2 items-center",
"top-right": "top-3 right-3 items-end",
"bottom-left": "bottom-3 left-3 items-start flex-col-reverse",
"bottom-center": "bottom-3 left-1/2 -translate-x-1/2 items-center flex-col-reverse",
"bottom-right": "bottom-3 right-3 items-end flex-col-reverse",
};
const CheckIcon: ReactNode = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="m8.5 12.2 2.4 2.4 4.6-5" />
</svg>
);
const AlertIcon: ReactNode = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 8v5" />
<path d="M12 16.5h.01" />
</svg>
);
const InfoIcon: ReactNode = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 11.5v5" />
<path d="M12 7.5h.01" />
</svg>
);
const WarnIcon: ReactNode = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M10.3 3.9 1.9 18a2 2 0 0 0 1.7 3h16.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
<path d="M12 9v4.5" />
<path d="M12 17.5h.01" />
</svg>
);
const TYPE_META: Record<
ToastType,
{ label: string; role: "status" | "alert"; chip: string; bar: string; icon: ReactNode }
> = {
success: {
label: "Success",
role: "status",
chip: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
bar: "bg-emerald-500 dark:bg-emerald-400",
icon: CheckIcon,
},
error: {
label: "Error",
role: "alert",
chip: "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
bar: "bg-rose-500 dark:bg-rose-400",
icon: AlertIcon,
},
info: {
label: "Info",
role: "status",
chip: "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
bar: "bg-sky-500 dark:bg-sky-400",
icon: InfoIcon,
},
warning: {
label: "Warning",
role: "alert",
chip: "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
bar: "bg-amber-500 dark:bg-amber-400",
icon: WarnIcon,
},
};
const MESSAGES: Record<ToastType, { title: string; description: string }[]> = {
success: [
{ title: "Payment received", description: "Invoice #4021 was charged to Visa ending 4242." },
{ title: "Deploy succeeded", description: "v2.8.1 is live on production — shipped in 41 seconds." },
{ title: "Saved to workspace", description: "Your edits to Q3-roadmap.md are synced across devices." },
],
error: [
{ title: "Upload failed", description: "product-hero.webp exceeded the 8 MB attachment limit." },
{ title: "Payment declined", description: "Card ending 1180 was rejected by the issuing bank." },
{ title: "Build broke", description: "3 type errors in checkout/session.ts — open logs to review." },
],
info: [
{ title: "Sync in progress", description: "Reconnecting to the Frankfurt (eu-central-1) region." },
{ title: "New reply", description: "Priya commented on the pricing-page thread." },
{ title: "Export ready", description: "Your 12,480-row CSV is ready to download." },
],
warning: [
{ title: "Storage almost full", description: "You've used 92% of your 50 GB plan." },
{ title: "Session expiring", description: "You'll be signed out after 5 minutes of inactivity." },
{ title: "Rate limit near", description: "312 of 500 API calls used this hour." },
],
};
export default function ToastPosition() {
const prefersReduced = useReducedMotion();
const [position, setPosition] = useState<Position>("bottom-right");
const [type, setType] = useState<ToastType>("success");
const [durationKey, setDurationKey] = useState<string>("5000");
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [paused, setPaused] = useState(false);
const idRef = useRef(0);
const msgIndexRef = useRef(0);
const pausedRef = useRef(false);
const timers = useRef<Map<number, TimerRecord>>(new Map());
const posRefs = useRef<(HTMLButtonElement | null)[]>([]);
const typeRefs = useRef<(HTMLButtonElement | null)[]>([]);
const duration = durationKey === "persist" ? Number.POSITIVE_INFINITY : Number(durationKey);
useEffect(() => {
const active = timers.current;
return () => {
active.forEach((t) => {
if (t.handle) clearTimeout(t.handle);
});
active.clear();
};
}, []);
const dismiss = (id: number) => {
const t = timers.current.get(id);
if (t && t.handle) clearTimeout(t.handle);
timers.current.delete(id);
setToasts((prev) => prev.filter((x) => x.id !== id));
};
const scheduleTimer = (id: number, ms: number) => {
if (!Number.isFinite(ms)) return;
if (pausedRef.current) {
timers.current.set(id, { handle: undefined, remaining: ms, startedAt: Date.now() });
return;
}
const handle = setTimeout(() => dismiss(id), ms);
timers.current.set(id, { handle, remaining: ms, startedAt: Date.now() });
};
const pauseTimers = () => {
if (pausedRef.current) return;
pausedRef.current = true;
const now = Date.now();
timers.current.forEach((t) => {
if (t.handle) clearTimeout(t.handle);
t.remaining = Math.max(0, t.remaining - (now - t.startedAt));
t.handle = undefined;
});
setPaused(true);
};
const resumeTimers = () => {
if (!pausedRef.current) return;
pausedRef.current = false;
const now = Date.now();
timers.current.forEach((t, id) => {
t.startedAt = now;
t.handle = setTimeout(() => dismiss(id), t.remaining);
});
setPaused(false);
};
const showToast = () => {
const id = idRef.current + 1;
idRef.current = id;
const pack = MESSAGES[type];
const msg = pack[msgIndexRef.current % pack.length];
msgIndexRef.current += 1;
const item: ToastItem = {
id,
type,
title: msg.title,
description: msg.description,
duration,
};
setToasts((prev) => {
const next = [item, ...prev];
next.slice(MAX_TOASTS).forEach((o) => {
const t = timers.current.get(o.id);
if (t && t.handle) clearTimeout(t.handle);
timers.current.delete(o.id);
});
return next.slice(0, MAX_TOASTS);
});
scheduleTimer(id, duration);
};
const clearAll = () => {
timers.current.forEach((t) => {
if (t.handle) clearTimeout(t.handle);
});
timers.current.clear();
pausedRef.current = false;
setPaused(false);
setToasts([]);
};
const onPositionKey = (e: KeyboardEvent<HTMLButtonElement>, i: number) => {
const col = i % 3;
const row = Math.floor(i / 3);
let nextCol = col;
let nextRow = row;
switch (e.key) {
case "ArrowRight":
nextCol = (col + 1) % 3;
break;
case "ArrowLeft":
nextCol = (col + 2) % 3;
break;
case "ArrowUp":
nextRow = 0;
break;
case "ArrowDown":
nextRow = 1;
break;
case "Home":
nextCol = 0;
nextRow = 0;
break;
case "End":
nextCol = 2;
nextRow = 1;
break;
default:
return;
}
e.preventDefault();
const nextIndex = nextRow * 3 + nextCol;
setPosition(POSITIONS[nextIndex]);
posRefs.current[nextIndex]?.focus();
};
const onTypeKey = (e: KeyboardEvent<HTMLButtonElement>, i: number) => {
const last = TYPES.length - 1;
let next = i;
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
next = i === last ? 0 : i + 1;
break;
case "ArrowLeft":
case "ArrowUp":
next = i === 0 ? last : i - 1;
break;
case "Home":
next = 0;
break;
case "End":
next = last;
break;
default:
return;
}
e.preventDefault();
setType(TYPES[next]);
typeRefs.current[next]?.focus();
};
return (
<section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 sm:py-20 lg:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes tpProgress {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
@keyframes tpPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.35; transform: scale(0.7); }
}
.tp-progress {
width: 100%;
transform-origin: left;
animation-name: tpProgress;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.tp-live-dot {
animation: tpPulse 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.tp-progress,
.tp-live-dot {
animation: none !important;
}
}
`}</style>
<div className="mx-auto max-w-6xl">
<header className="mb-10 max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
Notifications
</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
Toast position picker
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Pick a corner, choose a style, then fire a notification into the live preview. Toasts
stack, auto-dismiss, and pause while you hover — everything is keyboard operable.
</p>
</header>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* Controls */}
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm lg:col-span-2 dark:border-slate-800 dark:bg-slate-900">
<div className="space-y-6">
{/* Position */}
<div>
<div className="mb-2 flex items-baseline justify-between">
<span className="text-sm font-semibold">Position</span>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
{POSITION_LABEL[position]}
</span>
</div>
<div
role="radiogroup"
aria-label="Toast position"
className="grid aspect-[16/9] grid-cols-3 grid-rows-2 gap-2 rounded-xl border-2 border-dashed border-slate-200 bg-slate-50 p-2 dark:border-slate-700 dark:bg-slate-950/40"
>
{POSITIONS.map((p, i) => {
const selected = position === p;
return (
<button
key={p}
type="button"
role="radio"
aria-checked={selected}
aria-label={POSITION_LABEL[p]}
tabIndex={selected ? 0 : -1}
ref={(el) => {
posRefs.current[i] = el;
}}
onClick={() => setPosition(p)}
onKeyDown={(e) => onPositionKey(e, i)}
className={`group flex ${CELL_ALIGN[p]} rounded-lg border p-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
selected
? "border-indigo-500 bg-indigo-50 dark:border-indigo-400/70 dark:bg-indigo-500/10"
: "border-slate-200 bg-white hover:border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:hover:border-slate-600"
}`}
>
<span
className={`h-1.5 w-6 rounded-full transition ${
selected
? "bg-indigo-500 dark:bg-indigo-400"
: "bg-slate-300 group-hover:bg-slate-400 dark:bg-slate-600 dark:group-hover:bg-slate-500"
}`}
/>
</button>
);
})}
</div>
</div>
{/* Style */}
<div>
<span className="mb-2 block text-sm font-semibold">Style</span>
<div role="radiogroup" aria-label="Toast style" className="flex flex-wrap gap-2">
{TYPES.map((tp, i) => {
const meta = TYPE_META[tp];
const selected = type === tp;
return (
<button
key={tp}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
ref={(el) => {
typeRefs.current[i] = el;
}}
onClick={() => setType(tp)}
onKeyDown={(e) => onTypeKey(e, i)}
className={`inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
selected
? "border-indigo-500 bg-indigo-50 text-slate-900 dark:border-indigo-400/70 dark:bg-indigo-500/10 dark:text-white"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
}`}
>
<span
className={`inline-flex h-6 w-6 items-center justify-center rounded-md [&_svg]:h-4 [&_svg]:w-4 ${meta.chip}`}
>
{meta.icon}
</span>
{meta.label}
</button>
);
})}
</div>
</div>
{/* Duration */}
<div>
<label htmlFor="tp-duration" className="mb-2 block text-sm font-semibold">
Duration
</label>
<select
id="tp-duration"
value={durationKey}
onChange={(e) => setDurationKey(e.target.value)}
className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:focus-visible:ring-offset-slate-900"
>
<option value="3000">3 seconds</option>
<option value="5000">5 seconds</option>
<option value="8000">8 seconds</option>
<option value="persist">Until dismissed</option>
</select>
</div>
{/* Actions */}
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
onClick={showToast}
className="inline-flex flex-1 items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 active:bg-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M6 8a6 6 0 0 1 12 0c0 6.5 2.5 8.5 2.5 8.5H3.5S6 14.5 6 8" />
<path d="M10.2 20.5a2 2 0 0 0 3.6 0" />
</svg>
Show toast
</button>
<button
type="button"
onClick={clearAll}
disabled={toasts.length === 0}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Clear
{toasts.length > 0 ? ` (${toasts.length})` : ""}
</button>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400">
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-mono text-[10px] text-slate-600 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300">
Arrow keys
</kbd>{" "}
move the grid selection,{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-mono text-[10px] text-slate-600 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300">
Enter
</kbd>{" "}
fires the toast.
</p>
</div>
</div>
{/* Preview */}
<div className="lg:col-span-3">
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950">
{/* Browser chrome */}
<div className="flex items-center gap-3 border-b border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center gap-1.5" aria-hidden="true">
<span className="h-3 w-3 rounded-full bg-rose-400" />
<span className="h-3 w-3 rounded-full bg-amber-400" />
<span className="h-3 w-3 rounded-full bg-emerald-400" />
</div>
<div className="flex-1 truncate rounded-md bg-white px-3 py-1 text-center text-xs text-slate-500 ring-1 ring-slate-200 dark:bg-slate-950 dark:text-slate-400 dark:ring-slate-700">
app.northwind.dev/dashboard
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<span className="tp-live-dot h-2 w-2 rounded-full bg-emerald-500 dark:bg-emerald-400" />
Live
</div>
</div>
{/* Stage */}
<div className="relative h-[420px] overflow-hidden bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-950">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 text-slate-300 opacity-50 dark:text-slate-700 dark:opacity-40"
style={{
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "22px 22px",
}}
/>
{/* Mock app content */}
<div
aria-hidden="true"
className="absolute inset-0 flex flex-col items-center justify-center gap-4 p-8"
>
<div className="w-full max-w-sm rounded-xl border border-slate-200 bg-white/70 p-5 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/50">
<div className="mb-4 flex items-center gap-3">
<div className="h-9 w-9 rounded-lg bg-slate-200 dark:bg-slate-700" />
<div className="space-y-1.5">
<div className="h-2.5 w-28 rounded-full bg-slate-200 dark:bg-slate-700" />
<div className="h-2 w-20 rounded-full bg-slate-100 dark:bg-slate-800" />
</div>
</div>
<div className="space-y-2.5">
<div className="h-2.5 w-full rounded-full bg-slate-100 dark:bg-slate-800" />
<div className="h-2.5 w-5/6 rounded-full bg-slate-100 dark:bg-slate-800" />
<div className="h-2.5 w-4/6 rounded-full bg-slate-100 dark:bg-slate-800" />
</div>
</div>
<p className="text-xs font-medium text-slate-400 dark:text-slate-500">
Your app UI — toasts render on top
</p>
</div>
{/* Toast stack */}
<ul
aria-label="Notifications"
className={`pointer-events-none absolute z-10 flex w-[min(20rem,calc(100%-1.5rem))] list-none flex-col gap-2.5 ${ANCHOR_CLASS[position]}`}
>
<AnimatePresence initial={false}>
{toasts.map((t) => {
const meta = TYPE_META[t.type];
const enterY = position.startsWith("top") ? -18 : 18;
return (
<motion.li
key={t.id}
layout={!prefersReduced}
initial={
prefersReduced
? { opacity: 0 }
: { opacity: 0, y: enterY, scale: 0.97 }
}
animate={
prefersReduced
? { opacity: 1 }
: { opacity: 1, y: 0, scale: 1 }
}
exit={
prefersReduced
? { opacity: 0 }
: { opacity: 0, y: enterY, scale: 0.96 }
}
transition={{
duration: prefersReduced ? 0.14 : 0.32,
ease: [0.22, 1, 0.36, 1],
}}
onMouseEnter={pauseTimers}
onMouseLeave={resumeTimers}
onFocus={pauseTimers}
onBlur={resumeTimers}
role={meta.role}
aria-atomic="true"
className="pointer-events-auto relative w-full overflow-hidden rounded-xl border border-slate-200 bg-white/95 shadow-lg backdrop-blur dark:border-slate-700 dark:bg-slate-900/95"
>
<div className="flex items-start gap-3 p-3.5">
<span
className={`mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg [&_svg]:h-5 [&_svg]:w-5 ${meta.chip}`}
>
{meta.icon}
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-slate-900 dark:text-white">
{t.title}
</p>
<p className="mt-0.5 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
{t.description}
</p>
</div>
<button
type="button"
onClick={() => dismiss(t.id)}
aria-label={`Dismiss ${meta.label.toLowerCase()}: ${t.title}`}
className="-mr-1 -mt-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:hover:bg-slate-800 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
{Number.isFinite(t.duration) ? (
<span
aria-hidden="true"
className="absolute inset-x-0 bottom-0 block h-1 overflow-hidden"
>
<span
className={`tp-progress block h-full ${meta.bar}`}
style={{
animationDuration: `${t.duration}ms`,
animationPlayState: paused ? "paused" : "running",
}}
/>
</span>
) : null}
</motion.li>
);
})}
</AnimatePresence>
</ul>
</div>
</div>
</div>
</div>
</div>
</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

Glass Toast
Originalglassmorphic toasts

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.

