Toast Notification Stack
A queue of toast notifications that stack, reflow, and auto-dismiss independently.
- Category
- Patterns
- Added
- 2026-08-22
- Deps
- 1
- Updated
- 2026-09-01
Props
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
type Toast = { id: number; message: string };
let toastId = 0;
export function useToasts({
duration = 2200,
position = "bottom",
}: {
/** ms before a toast auto-dismisses. */
duration?: number;
/** Which edge the stack grows from. */
position?: "bottom" | "top";
} = {}) {
const [toasts, setToasts] = useState<Toast[]>([]);
function pushToast(message: string) {
const id = toastId++;
setToasts((prev) => [...prev, { id, message }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, duration);
}
const ToastStack = () => (
<div
className={
"pointer-events-none fixed inset-x-0 z-50 flex flex-col items-center gap-2 " +
(position === "bottom" ? "bottom-4" : "top-4")
}
>
<AnimatePresence initial={false}>
{toasts.map((t) => (
<motion.div
key={t.id}
layout
initial={{ opacity: 0, y: position === "bottom" ? 16 : -16, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="rounded-full bg-neutral-900 px-4 py-2 text-xs font-medium text-white shadow-lg"
>
{t.message}
</motion.div>
))}
</AnimatePresence>
</div>
);
return { pushToast, ToastStack };
}
About this pattern
Reaching for sonner or react-hot-toast is usually the right call for a production toast system, but when that dependency isn't wanted, this is a from-scratch hook plus a stack renderer built on useState and AnimatePresence alone. The detail worth copying carefully: each toast schedules its own removal timer keyed to its own id, so one toast's dismissal never resets or cancels another's — a single shared timer is a common bug in quick implementations, where pushing a new toast accidentally extends or shortens an existing one's lifetime. The layout prop on each toast element is what makes the remaining toasts smoothly slide into the gap when one above them disappears, instead of snapping to a new position. Reach for this over the full libraries when the needs are simple: no swipe-to-dismiss, no promise-based API, no positioning presets. The gaps: no de-duplication, no cap on how many toasts can stack at once, and the id counter is a module-level counter, fine for one client bundle but not something to rely on across multiple bundle instances.
Curator’s note
Each toast's dismiss timer is scoped to its own id, not a shared timeout — a common bug in quick toast implementations is one shared timer that makes an earlier toast vanish early (or a later one linger) whenever a new toast is pushed.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Peeking Toast Stack
patternsToasts collapsed into a depth-scaled deck that fans out on hover.
Undo Commit Toast
patternsAn undo window that defers the action instead of reversing it, and pauses when you tab toward the button.
Spotlight Card Grid
patternsA card grid with a soft red spotlight that follows the cursor across it.