Skip to content

Toast Notification Stack

A queue of toast notifications that stack, reflow, and auto-dismiss independently.

Freeminimalglass
Category
Patterns
Added
2026-08-22
Deps
1
Updated
2026-09-01
Preview

Props

2200
Requires framer-motion
"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 };
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

Toasts collapsed into a depth-scaled deck that fans out on hover.

toaststacknotification
Featured
NewPro

An undo window that defers the action instead of reversing it, and pauses when you tab toward the button.

undotoastasync
Featured

A card grid with a soft red spotlight that follows the cursor across it.

gridspotlightcursor