Toast
Lightweight notification surface for async feedback, confirmations, and background status updates. It supports four semantic variants, animated entry and exit, and portal rendering for reliable stacking.
Install
npx react-principles add toastTheme Preview
All four variants are shown in forced light and dark surfaces so the semantic color differences remain visible regardless of the current app theme.
Draft saved
Your local changes were stored automatically.
Published successfully
Your latest release is now visible to your team.
Action required
Reconnect your integration to keep sync running.
Upload failed
The file could not be processed. Try again in a moment.
Draft saved
Your local changes were stored automatically.
Published successfully
Your latest release is now visible to your team.
Action required
Reconnect your integration to keep sync running.
Upload failed
The file could not be processed. Try again in a moment.
Live Demo
Each trigger opens a dedicated toast variant. Watch the viewport corners to see the portal-mounted notification appear and dismiss.
Code Snippet
import { Toast } from "@/ui/Toast"; <Toast open={open} onOpenChange={setOpen} variant="success"> <Toast.Title>Saved successfully</Toast.Title> <Toast.Description>Your profile changes are now live.</Toast.Description> <Toast.Footer> <Toast.Close>Dismiss</Toast.Close> </Toast.Footer> </Toast>
Copy-Paste (Single File)
"use client"; import { createContext, useContext, useEffect, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { useAnimatedMount } from "@/hooks/use-animated-mount"; import { cn } from "@/lib/utils"; export type ToastVariant = "default" | "success" | "warning" | "error"; export type ToastPosition = "top-right" | "bottom-right" | "top-left" | "bottom-left"; interface ToastContextValue { onClose: () => void; } const ToastContext = createContext<ToastContextValue | null>(null); function useToastContext() { const context = useContext(ToastContext); if (!context) throw new Error("Toast sub-components must be used inside <Toast>"); return context; } export interface ToastProps { open: boolean; onOpenChange: (open: boolean) => void; duration?: number; variant?: ToastVariant; position?: ToastPosition; children: ReactNode; className?: string; } const VARIANT_CLASSES: Record<ToastVariant, string> = { default: "border-slate-200 bg-white dark:border-[#1f2937] dark:bg-[#161b22]", success: "border-green-300 bg-green-50 dark:border-green-900 dark:bg-green-950/30", warning: "border-amber-300 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30", error: "border-red-300 bg-red-50 dark:border-red-900 dark:bg-red-950/30", }; const POSITION_CLASSES: Record<ToastPosition, string> = { "top-right": "right-4 top-4", "bottom-right": "right-4 bottom-4", "top-left": "left-4 top-4", "bottom-left": "left-4 bottom-4", }; export function Toast({ open, onOpenChange, duration = 3000, variant = "default", position = "top-right", children, className, }: ToastProps) { const { mounted, visible } = useAnimatedMount(open, 180); useEffect(() => { if (!open || duration <= 0) return; const timeout = setTimeout(() => onOpenChange(false), duration); return () => clearTimeout(timeout); }, [open, duration, onOpenChange]); useEffect(() => { if (!open) return; const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") onOpenChange(false); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [open, onOpenChange]); if (!mounted) return null; return createPortal( <ToastContext.Provider value={{ onClose: () => onOpenChange(false) }}> <div className={cn("fixed z-[70] w-full max-w-sm", POSITION_CLASSES[position])}> <div role="status" className={cn( "rounded-xl border p-4 shadow-xl transition-all", VARIANT_CLASSES[variant], visible ? "translate-y-0 opacity-100" : "translate-y-2 opacity-0", className )} > {children} </div> </div> </ToastContext.Provider>, document.body ); }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls whether the toast is mounted and visible. |
onOpenChange | (open: boolean) => void | — | Called when auto-dismiss, Escape, or close actions change visibility. |
variant | "default" | "success" | "warning" | "error" | "default" | Changes the semantic surface colors of the toast. |
position | "top-right" | "bottom-right" | "top-left" | "bottom-left" | "top-right" | Places the portal-mounted toast in one of four screen corners. |
duration | number | 3000 | Auto-dismiss delay in milliseconds. Use 0 or less to disable auto close. |
className | string | — | Additional classes applied to the toast surface. |