typescriptintermediate

Toast Notification System

Build a toast notification system with auto-dismiss, stacking, animations, and severity levels.

typescript
import { createContext, useCallback, useContext, useState, useRef, useEffect } from 'react';

type ToastType = 'success' | 'error' | 'warning' | 'info';

interface Toast {
  id: string;
  message: string;
  type: ToastType;
  duration: number;
}

interface ToastContextType {
  toast: (message: string, type?: ToastType, duration?: number) => void;
}

const ToastContext = createContext<ToastContextType | null>(null);

const typeStyles: Record<ToastType, string> = {
  success: 'bg-green-900/90 border-green-500/50 text-green-200',
  error: 'bg-red-900/90 border-red-500/50 text-red-200',
  warning: 'bg-yellow-900/90 border-yellow-500/50 text-yellow-200',
  info: 'bg-blue-900/90 border-blue-500/50 text-blue-200',
};

const icons: Record<ToastType, string> = {
  success: '\u2713', error: '\u2717', warning: '\u26A0', info: '\u2139',
};

function ToastProvider({ children }: { children: React.ReactNode }) {
  const [toasts, setToasts] = useState<Toast[]>([]);
  const timers = useRef(new Map<string, NodeJS.Timeout>());

  const removeToast = useCallback((id: string) => {
    setToasts((prev) => prev.filter((t) => t.id !== id));
    const timer = timers.current.get(id);
    if (timer) { clearTimeout(timer); timers.current.delete(id); }
  }, []);

  const toast = useCallback((message: string, type: ToastType = 'info', duration = 4000) => {
    const id = crypto.randomUUID();
    setToasts((prev) => [...prev, { id, message, type, duration }]);
    const timer = setTimeout(() => removeToast(id), duration);
    timers.current.set(id, timer);
  }, [removeToast]);

  return (
    <ToastContext.Provider value={{ toast }}>
      {children}
      <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
        {toasts.map((t) => (
          <div
            key={t.id}
            className={`flex items-center gap-2 px-4 py-3 rounded-lg border shadow-lg
              animate-[slideIn_0.3s_ease] ${typeStyles[t.type]}`}
            role="alert"
          >
            <span className="text-lg">{icons[t.type]}</span>
            <span className="flex-1 text-sm">{t.message}</span>
            <button
              onClick={() => removeToast(t.id)}
              className="opacity-60 hover:opacity-100 text-lg leading-none"
            >
              &times;
            </button>
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
}

function useToast() {
  const ctx = useContext(ToastContext);
  if (!ctx) throw new Error('useToast must be within ToastProvider');
  return ctx;
}

// Usage:
// <ToastProvider><App /></ToastProvider>
// const { toast } = useToast();
// toast('Saved!', 'success');
// toast('Network error', 'error', 6000);

export { ToastProvider, useToast };

Use Cases

  • User feedback notifications
  • Form submission confirmations
  • Error state communication

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.