/ web / src / components / Toast.tsx
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  }