ToastProvider / useToast
LG · 3300B gzip budgetA toast queue with correctly-timed live regions.
- Category
- Feedback & status
- Budget
- lg, composes several components
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureTry the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/toast.jsonCopies the source into your project. Pulls in 3 shared registry items: cn, portal, use-hydrated.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import { createContext, useCallback, useContext, useId, useMemo, useState } from "react";
import { cn } from "../lib/cn";
import { Portal } from "./portal";
export type ToastTone = "neutral" | "success" | "danger";
interface ToastItem {
id: string;
message: string;
tone: ToastTone;
}
interface ToastContextValue {
show: (message: string, tone?: ToastTone) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
const TONES: Record<ToastTone, string> = {
neutral: "bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900",
success: "bg-green-700 text-white",
danger: "bg-red-700 text-white",
};
/**
* Toast queue and viewport. Each toast is its own live region — mounted
* empty, then filled a frame later, matching the timing that makes
* `announce()` actually audible — with `assertive` reserved for `danger`
* toasts, since a routine success message should not interrupt anything.
*/
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const baseId = useId();
const show = useCallback((message: string, tone: ToastTone = "neutral") => {
const id = `${baseId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
setToasts((prev) => [...prev, { id, message, tone }]);
setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 5000);
}, [baseId]);
const value = useMemo(() => ({ show }), [show]);
return (
<ToastContext.Provider value={value}>
{children}
<Portal>
<div className="fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-2 px-4">
{toasts.map((toast) => (
<div
key={toast.id}
role={toast.tone === "danger" ? "alert" : "status"}
className={cn("rounded-lg px-4 py-2.5 text-sm shadow-lg", TONES[toast.tone])}
>
{toast.message}
</div>
))}
</div>
</Portal>
</ToastContext.Provider>
);
}
export function useToast(): ToastContextValue {
const context = useContext(ToastContext);
if (!context) throw new Error("useToast must be used within a ToastProvider");
return context;
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation