Toast.tsx
1 import { useEffect, useState } from "react"; 2 import { createPortal } from "react-dom"; 3 4 export function Toast({ toast }: { toast: { message: string; type: "success" | "error" } | null }) { 5 const [visible, setVisible] = useState(false); 6 const [current, setCurrent] = useState(toast); 7 8 useEffect(() => { 9 if (toast) { 10 setCurrent(toast); 11 setVisible(true); 12 } else { 13 setVisible(false); 14 const timer = setTimeout(() => setCurrent(null), 200); 15 return () => clearTimeout(timer); 16 } 17 }, [toast]); 18 19 if (!current) return null; 20 21 // Portal to document.body so the toast escapes any ancestor stacking context 22 // (e.g. <main> has `relative z-2`, which would trap z-50 below the header's z-40). 23 return createPortal( 24 <div 25 role="status" 26 aria-live="polite" 27 className={`fixed top-16 right-4 z-50 border px-4 py-2.5 font-courier text-xs tracking-wider uppercase backdrop-blur-sm ${ 28 current.type === "success" 29 ? "bg-success/15 text-success border-success/30" 30 : "bg-destructive/15 text-destructive border-destructive/30" 31 }`} 32 style={{ 33 animation: visible ? "toast-in 200ms ease-out forwards" : "toast-out 200ms ease-in forwards", 34 }} 35 > 36 {current.message} 37 </div>, 38 document.body, 39 ); 40 }