Toast Notifications

Featured

by Dax · tsx · 50 lines

A toast notification system with a hook for easy use anywhere. Slides in from the bottom-right, auto-dismisses after 4 seconds, and stacks neatly when multiple fire at once.

toastnotificationhookradix
50 lines
"use client";
import * as Toast from "@radix-ui/react-toast";
import { createContext, useContext, useState, useCallback } from "react";
import { X } from "lucide-react";

type ToastData = { id: number; title: string; description?: string };
const Ctx = createContext<(t: Omit<ToastData, "id">) => void>(() => {});
export const useToast = () => useContext(Ctx);
let nextId = 0;

export function ToastProvider({ children }: { children: React.ReactNode }) {
  const [toasts, setToasts] = useState<ToastData[]>([]);

  const toast = useCallback((t: Omit<ToastData, "id">) => {
    setToasts((prev) => [...prev, { ...t, id: ++nextId }]);
  }, []);

  return (
    <Ctx.Provider value={toast}>
      <Toast.Provider duration={4000}>
        {children}
        {toasts.map((t) => (
          <Toast.Root
            key={t.id}
            onOpenChange={(open) => {
              if (!open) setToasts((p) => p.filter((x) => x.id !== t.id));
            }}
            className="rounded-xl border border-neutral-200 bg-white p-4 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full"
          >
            <div className="flex items-start gap-3">
              <div className="flex-1">
                <Toast.Title className="text-sm font-semibold">{t.title}</Toast.Title>
                {t.description && (
                  <Toast.Description className="mt-1 text-xs text-neutral-500">
                    {t.description}
                  </Toast.Description>
                )}
              </div>
              <Toast.Close className="text-neutral-400 hover:text-neutral-600">
                <X size={14} />
              </Toast.Close>
            </div>
          </Toast.Root>
        ))}
        <Toast.Viewport className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-80" />
      </Toast.Provider>
    </Ctx.Provider>
  );
}