All notes
July 9, 2026 · 7 min
Toast notifications with a tiny React context
A queue, a portal, and a timeout. No toast library. Just a provider you can drop around the app.
- React
- TypeScript
Try it outClick around. This is the real component.
toast.tsxTSX
Preview
1"use client";2 3import { createContext, useCallback, useContext, useMemo, useState } from "react";4import { createPortal } from "react-dom";5 6type Toast = { id: number; message: string };7type ToastContextValue = { push: (message: string) => void };8 9const ToastContext = createContext<ToastContextValue | null>(null);10 11export function ToastProvider({ children }: { children: React.ReactNode }) {12 const [toasts, setToasts] = useState<Toast[]>([]);13 14 const push = useCallback((message: string) => {15 const id = Date.now() + Math.random();16 setToasts((current) => [...current, { id, message }]);17 window.setTimeout(() => {18 setToasts((current) => current.filter((toast) => toast.id !== id));19 }, 3200);20 }, []);21 22 const value = useMemo(() => ({ push }), [push]);23 24 return (25 <ToastContext.Provider value={value}>26 {children}27 {typeof document !== "undefined"28 ? createPortal(29 <div30 className="pointer-events-none fixed right-4 bottom-4 z-50 flex w-80 flex-col gap-2"31 aria-live="polite"32 aria-relevant="additions"33 >34 {toasts.map((toast) => (35 <div36 key={toast.id}37 className="rounded-xl border border-white/10 bg-zinc-950 px-4 py-3 text-sm shadow-2xl"38 >39 {toast.message}40 </div>41 ))}42 </div>,43 document.body,44 )45 : null}46 </ToastContext.Provider>47 );48}49 50export function useToast() {51 const context = useContext(ToastContext);52 if (!context) {53 throw new Error("useToast must be used inside ToastProvider");54 }55 return context;56}Toasts are a global concern: any button, any form, any fetch should be able to say “saved” without threading a setter down the tree. That is a context, not a component prop.
The implementation is a list of messages, a push function, and a portal that renders them in a live region so screen readers hear the update.
Firing a toast
example.tsxTSX
1"use client";2 3import { useToast } from "./toast";4 5export function SaveButton() {6 const { push } = useToast();7 8 return (9 <button10 type="button"11 onClick={() => push("Saved. The queue will dismiss this in a few seconds.")}12 >13 Save14 </button>15 );16}Where this stops being enough
Add variants (success or danger), a timer that pauses on hover, and an undo action when you need them. Until then, this is the entire feature: a queue and a portal. Most apps never outgrow it.