Building a toast notification in react
Toast notification component built with react context provider and motion
November 20, 2025
Toast notifications are small, temporary messages that give quick feedback , things like “Saved”, “Error”, or “Info”.
They are non-intrusive, self-dismissing, and help users stay in flow.
Here’s a simple breakdown of the anatomy and the implementation of a toast system.
Anatomy of a Toast
Here’s a toast broken into its core parts:
- Status Icon — instantly communicates the type (info, error, warning, success).
- Title — short, bold message.
- Description (optional) — extra details when needed.
- Close Button — lets the user manually dismiss.
- Auto-Dismiss Timer — toast fades out after a few seconds.
Perfect for delivering quick, contextual feedback without blocking the UI.
Toast System Architecture
A simple toast system usually has:
- Toast Provider — manages all toasts globally.
- Toast Stack — positions your toasts (top-right, top-center, etc.).
- Animation Layer — slide & fade using motion.
- Auto-Dismiss Logic — timer that pauses on hover.
- Accessible Alerts — uses role="alert" and clear icons.
Now let’s look at the code.
Toast Provider
Creates a global context and exposes the showToast() function.
// ToastProvider.tsx"use client";import { useState, createContext, useContext, ReactNode } from "react";import ToastNotification from "@/components/ui-components/ToastNotification";import { AnimatePresence } from "motion/react";import cn from "../lib/cn";export type ToastPosition =| "top-right"| "top-left"| "top-center"| "bottom-right"| "bottom-left"| "bottom-center";export interface toastTypes {id: number;type: "warning" | "success" | "error" | "neutral";title: string;message?: string;}type ToastContextType = {showToast: (title: string,type: toastTypes["type"],message?: string,) => void;} | null;const ToastContext = createContext<ToastContextType>(null);const positionStyles = {"top-right": "top-4 right-4","top-left": "top-4 left-4","top-center": "top-4 left-1/2 -translate-x-1/2","bottom-right": "bottom-4 right-4","bottom-left": "bottom-4 left-4","bottom-center": "bottom-4 left-1/2 -translate-x-1/2",};export const ToastProvider = ({children,position = "top-right",duration = 5000,}: {children: ReactNode;position?: ToastPosition;duration?: number;}) => {const [toasts, setToasts] = useState<toastTypes[]>([]);const showToast = (title: string,type: toastTypes["type"],message?: string,) => {const newToast = { id: Date.now(), type, title, message };setToasts((prev) => [newToast, ...prev]);};const removeToast = (id: number) => {setToasts((prev) => prev.filter((t) => t.id !== id));};return (<ToastContext.Provider value={{ showToast }}>{children}<divclassName={cn("fixed z-999 flex flex-col gap-2",positionStyles[position],)}><AnimatePresence>{toasts.map((toast) => (<ToastNotificationkey={toast.id}{...toast}onDismiss={removeToast}duration={duration}position={position}/>))}</AnimatePresence></div></ToastContext.Provider>);};export const useToast = () => {const ctx = useContext(ToastContext);if (!ctx) throw new Error("useToast must be used inside ToastProvider");return ctx;};
Toast Component
Handles the UI, animations, and auto-dismiss timer.
// ToastNotification.tsx"use client";import {FaSkull,FaCircleInfo,FaCircleRadiation,FaChampagneGlasses,FaXmark,} from "react-icons/fa6";import { motion } from "motion/react";import cn from "@/app/lib/cn";import { ToastPosition } from "@/app/providers/ToastProvider";import { useEffect, useRef } from "react";const toastConfig = {success: {icon: FaChampagneGlasses,className: "text-green-300 bg-green-700",},error: { icon: FaSkull, className: "text-red-100 bg-red-600" },warning: {icon: FaCircleRadiation,className: "text-neutral-900 bg-yellow-400",},neutral: { icon: FaCircleInfo, className: "text-blue-50 bg-blue-500" },};const toastAnimation = {"top-left": { opacity: 0, x: -50, filter: "blur(10px)" },"top-center": { opacity: 0, y: -50 },"top-right": { opacity: 0, x: 50 },"bottom-left": { opacity: 0, x: -50 },"bottom-center": { opacity: 0, y: 50 },"bottom-right": { opacity: 0, x: 50 },};export default function ToastNotification({id,type,title,message,onDismiss,duration,position,}: {id: number;type: "warning" | "success" | "error" | "neutral";title: string;message?: string;duration: number;position: ToastPosition;onDismiss: (id: number) => void;}) {const { icon: Icon, className } = toastConfig[type];const timerId = useRef<NodeJS.Timeout | null>(null);const remaining = useRef(duration);const started = useRef(Date.now());const pause = () => {if (timerId.current) clearTimeout(timerId.current);remaining.current -= Date.now() - started.current;};const resume = () => {started.current = Date.now();timerId.current = setTimeout(() => onDismiss(id), remaining.current);};useEffect(() => {resume();return () => timerId.current && clearTimeout(timerId.current);}, []);return (<motion.divlayoutrole="alert"initial={toastAnimation[position]}animate={{ opacity: 1, x: 0, y: 0, filter: "blur(0px)" }}exit={{opacity: 0,scale: 0.5,filter: "blur(20px)",transition: { duration: 0.2 },}}transition={{ duration: 0.5, type: "tween" }}onMouseEnter={pause}onMouseLeave={resume}className="toast-notification max-w-80 min-w-80 rounded-xl border bg-neutral-100 p-1 dark:border-neutral-700 dark:bg-neutral-800"><div className="group flex items-start justify-between gap-4 p-1"><div className="flex items-start gap-4"><div className={cn("rounded-lg p-1", className)}><Icon width={16} /></div><div className="flex flex-col gap-1"><h6>{title}</h6>{message && <p className="text-sm">{message}</p>}</div></div><buttonclassName="p-1 opacity-0 group-hover:opacity-90"onClick={() => onDismiss(id)}><FaXmark /></button></div><divclassName="absolute bottom-0 left-0 h-0.5 bg-neutral-400/50"style={{ animationDuration: `${duration}ms` }}/></motion.div>);}
Using the Toast
A simple demo component:
// ToastDemo.tsx"use client";import { useToast } from "@/app/providers/ToastProvider";import Button from "../ui/Button";export default function ToastDemo() {const { showToast } = useToast();return (<div className="flex flex-col items-center gap-2"><Button onClick={() => showToast("Information", "neutral")}>Info</Button><Button onClick={() => showToast("Warning!", "warning")}>Warning</Button><Button onClick={() => showToast("Success!", "success")}>Success</Button><Button onClick={() => showToast("Error!", "error")}>Error</Button></div>);}
Provider in layout
import { ToastProvider } from "./providers/ToastProvider";...<ToastProvider position="bottom-left" duration={5000}><Navbar />{children}...</ToastProvider>...
Conclusion
Toast notifications may look small, but building a good system requires thoughtful design, from icons and animation to auto-dismiss logic and global state handling.