UI
Toast
A lightweight notification component.
import { Button } from '@/components/Button';import { Toaster } from '@/components/Toaster';import { useToast } from '@/lib/useToast';
export const ToasterHero = () => { const { toast } = useToast() return ( <> <Toaster /> <Button className="mx-auto flex" onClick={() => toast({ title: "🍞", description: "Here is your toast.", }) } > Make me a toast </Button> </> )};
Installation
npm install @radix-ui/react-toast @remixicon/react
- Copy and paste the code into your project’s component directory. Do not forget to update the import paths.
// Tremor Toast [v0.0.4] import React from "react"import * as ToastPrimitives from "@radix-ui/react-toast"import { RiCheckboxCircleFill, RiCloseCircleFill, RiErrorWarningFill, RiInformationFill, RiLoader2Fill,} from "@remixicon/react" import { cx } from "@/lib/utils" const ToastProvider = ToastPrimitives.ProviderToastProvider.displayName = "ToastProvider" const ToastViewport = React.forwardRef< React.ElementRef<typeof ToastPrimitives.Viewport>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>>(({ className, ...props }, forwardedRef) => ( <ToastPrimitives.Viewport ref={forwardedRef} className={cx( "fixed right-0 top-0 z-[9999] m-0 flex w-full max-w-[100vw] list-none flex-col gap-2 p-[var(--viewport-padding)] [--viewport-padding:_15px] sm:max-w-md sm:gap-4", className, )} {...props} />)) ToastViewport.displayName = "ToastViewport" interface ActionProps { label: string altText: string onClick: () => void | Promise<void>} interface ToastProps extends React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> { variant?: "info" | "success" | "warning" | "error" | "loading" title?: string description?: string action?: ActionProps disableDismiss?: boolean} const Toast = React.forwardRef< React.ElementRef<typeof ToastPrimitives.Root>, ToastProps>( ( { className, variant, title, description, action, disableDismiss = false, ...props }: ToastProps, forwardedRef, ) => { let Icon: React.ReactNode switch (variant) { case "success": Icon = ( <RiCheckboxCircleFill className="size-5 shrink-0 text-emerald-600 dark:text-emerald-500" aria-hidden="true" /> ) break case "warning": Icon = ( <RiErrorWarningFill className="size-5 shrink-0 text-amber-500 dark:text-amber-500" aria-hidden="true" /> ) break case "error": Icon = ( <RiCloseCircleFill className="size-5 shrink-0 text-red-600 dark:text-red-500" aria-hidden="true" /> ) break case "loading": Icon = ( <RiLoader2Fill className="size-5 shrink-0 animate-spin text-gray-600 dark:text-gray-500" aria-hidden="true" /> ) break default: Icon = ( <RiInformationFill className="size-5 shrink-0 text-blue-500 dark:text-blue-500" aria-hidden="true" /> ) break } return ( <ToastPrimitives.Root ref={forwardedRef} className={cx( // base "flex h-fit min-h-16 w-full overflow-hidden rounded-md border shadow-lg shadow-black/5", // background color "bg-white dark:bg-[#090E1A]", // border color "border-gray-200 dark:border-gray-800", // swipe "data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none", // transition "data-[state=open]:animate-slideLeftAndFade", "data-[state=closed]:animate-hide", className, )} tremor-id="tremor-raw" {...props} > <div className={cx( // base "flex flex-1 items-start gap-3 p-4", // border !disableDismiss || action ? "border-r border-gray-200 dark:border-gray-800" : "", )} > {Icon} <div className="flex flex-col gap-1"> {title && ( <ToastPrimitives.Title className="text-sm font-semibold text-gray-900 dark:text-gray-50"> {title} </ToastPrimitives.Title> )} {description && ( <ToastPrimitives.Description className="text-sm text-gray-600 dark:text-gray-400"> {description} </ToastPrimitives.Description> )} </div> </div> <div className="flex flex-col"> {action && ( <> <ToastPrimitives.Action altText={action.altText} className={cx( // base "flex flex-1 items-center justify-center px-6 text-sm font-semibold transition-colors", // hover "hover:bg-gray-50 hover:dark:bg-gray-900/30", // text color "text-gray-800 dark:text-gray-100", // active "active:bg-gray-100 active:dark:bg-gray-800", { "text-red-600 dark:text-red-500": variant === "error", }, )} onClick={(event) => { event.preventDefault() action.onClick() }} type="button" > {action.label} </ToastPrimitives.Action> <div className="h-px w-full bg-gray-200 dark:bg-gray-800" /> </> )} {!disableDismiss && ( <ToastPrimitives.Close className={cx( // base "flex flex-1 items-center justify-center px-6 text-sm transition-colors", // text color "text-gray-600 dark:text-gray-400", // hover "hover:bg-gray-50 hover:dark:bg-gray-900/30", // active "active:bg-gray-100", action ? "h-1/2" : "h-full", )} aria-label="Close" > Close </ToastPrimitives.Close> )} </div> </ToastPrimitives.Root> ) },)Toast.displayName = "Toast" type ToastActionElement = ActionProps export { Toast, ToastProvider, ToastViewport, type ToastActionElement, type ToastProps,}
- Copy and paste the code into your project’s component directory. Do not forget to update the import paths.
// Tremor Toaster [v0.0.0] "use client" import { useToast } from "@/lib/useToast" import { Toast, ToastProvider, ToastViewport } from "./Toast" const Toaster = () => { const { toasts } = useToast() return ( <ToastProvider swipeDirection="right"> {toasts.map(({ id, ...props }) => { return <Toast key={id} {...props} /> })} <ToastViewport /> </ToastProvider> )} export { Toaster }
- Copy and paste the code into your project’s hooks or component directory. Do not forget to update the import paths.
import React from "react" import type { ToastActionElement, ToastProps } from "@/components/Toast" const TOAST_LIMIT = 4const TOAST_REMOVE_DELAY = 1000000 type ToasterToast = ToastProps & { id: string title?: React.ReactNode description?: React.ReactNode action?: ToastActionElement} const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST",} as const let count = 0 function genId() { count = (count + 1) % Number.MAX_VALUE return count.toString()} type ActionType = typeof actionTypes type Action = | { type: ActionType["ADD_TOAST"] toast: ToasterToast } | { type: ActionType["UPDATE_TOAST"] toast: Partial<ToasterToast> } | { type: ActionType["DISMISS_TOAST"] toastId?: ToasterToast["id"] } | { type: ActionType["REMOVE_TOAST"] toastId?: ToasterToast["id"] } interface State { toasts: ToasterToast[]} const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>() const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { return } const timeout = setTimeout(() => { toastTimeouts.delete(toastId) dispatch({ type: "REMOVE_TOAST", toastId: toastId, }) }, TOAST_REMOVE_DELAY) toastTimeouts.set(toastId, timeout)} export const reducer = (state: State, action: Action): State => { switch (action.type) { case "ADD_TOAST": return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), } case "UPDATE_TOAST": return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t, ), } case "DISMISS_TOAST": { const { toastId } = action if (toastId) { addToRemoveQueue(toastId) } else { state.toasts.forEach((toast) => { addToRemoveQueue(toast.id) }) } return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t, ), } } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, toasts: [], } } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), } }} const listeners: Array<(state: State) => void> = [] let memoryState: State = { toasts: [] } // Updated with https://github.com/shadcn-ui/ui/pull/1038/filesfunction dispatch(action: Action) { if (action.type === "ADD_TOAST") { const toastExists = memoryState.toasts.some((t) => t.id === action.toast.id) if (toastExists) { return } } memoryState = reducer(memoryState, action) listeners.forEach((listener) => { listener(memoryState) })} type Toast = Omit<ToasterToast, "id"> function toast({ ...props }: Toast & { id?: string }) { const id = props?.id || genId() const update = (props: Toast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, }) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) dispatch({ type: "ADD_TOAST", toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() }, }, }) return { id: id, dismiss, update, }} function useToast() { const [state, setState] = React.useState<State>(memoryState) React.useEffect(() => { listeners.push(setState) return () => { const index = listeners.indexOf(setState) if (index > -1) { listeners.splice(index, 1) } } }, [state]) return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), }} export { toast, useToast }
import type { Config } from 'tailwindcss';export default { // ... theme: { extend: { keyframes: { hide: { from: { opacity: "1" }, to: { opacity: "0" }, }, slideLeftAndFade: { from: { opacity: "0", transform: "translateX(6px)" }, to: { opacity: "1", transform: "translateX(0)" }, }, }, animation: { hide: "hide 150ms cubic-bezier(0.16, 1, 0.3, 1)", slideLeftAndFade: "slideLeftAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", }, }, }, // ...} satisfies Config;
Example: Toast with variant Info and custom duration
import { Button } from '@/components/Button';import { Toaster } from '@/components/Toaster';import { useToast } from '@/lib/useToast';
const ToastPreview = () => ( <ToastProvider> <Toast open title="Info" description="Your account has been successfully created. Check your email for further instructions." variant="info" className="mt-0" /> </ToastProvider>)
export function ToastInfoExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Info", description: "Your account has been successfully created. Check your email for further instructions.", variant: "info", duration: 3000, }) } > Create toast </Button> </> )}
Example: Toast with variant warning and custom duration
import { Button } from "@/components/Button"import { Toast, ToastProvider } from "@/components/Toast"import { Toaster } from "@/components/Toaster"import { useToast } from "@/lib/useToast"
const ToastPreview = () => ( <ToastProvider> <Toast open title="Warning" description="Your trial period will expire in 3 days. Upgrade now to continue using our services." variant="warning" className="mt-0" /> </ToastProvider>)
export function ToastWarningExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Warning", description: "Your trial period will expire in 3 days. Upgrade now to continue using our services.", variant: "warning", duration: 3000, }) } > Create toast </Button> </> )}
Example: Toast with variant Loading and custom duration
import { Button } from "@/components/Button"import { Toast, ToastProvider } from "@/components/Toast"import { Toaster } from "@/components/Toaster"import { useToast } from "@/lib/useToast"
const ToastPreview = () => ( <ToastProvider> <Toast open title="Loading" description="Transaction in progress. Please wait up to 5 minutes to complete." variant="loading" className="mt-0" /> </ToastProvider>)
export function ToastLoadingExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Loading", description: "Transaction in progress. Please wait up to 5 minutes to complete.", variant: "loading", duration: 3000, }) } > Create toast </Button> </> )}
Example: Toast with variant Success and custom duration
import { Button } from "@/components/Button"import { Toast, ToastProvider } from "@/components/Toast"import { Toaster } from "@/components/Toaster"import { useToast } from "@/lib/useToast"
const ToastPreview = () => ( <ToastProvider> <Toast open title="Success" description="Your payment has been processed. Thank you for your purchase!" variant="success" className="mt-0" /> </ToastProvider>)
export function ToastSuccessExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Success", description: "Your payment has been processed. Thank you for your purchase!", variant: "success", duration: 3000, }) } > Create toast </Button> </> )}
Example: Toast with variant Error and custom duration
import { Button } from "@/components/Button"import { Toast, ToastProvider } from "@/components/Toast"import { Toaster } from "@/components/Toaster"import { useToast } from "@/lib/useToast"
const ToastPreview = () => ( <ToastProvider> <Toast open title="Error" description="Your trial period will expire in 3 days. Upgrade now to continue using our services." variant="error" className="mt-0" /> </ToastProvider>)
export function ToastErrorExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Error", description: "Your trial period will expire in 3 days. Upgrade now to continue using our services.", variant: "error", duration: 3000, }) } > Create toast </Button> </> )}
Example: Toast with variant Error and action
import { Button } from '@/components/Button';import { Toaster } from '@/components/Toaster';import { Toast, ToastProvider } from "@/components/Toast"import { useToast } from '@/lib/useToast';
const ToastPreview = () => ( <ToastProvider> <Toast open title="Error" description="Are you sure you want to transfer ownership to Max?" variant="error" className="mt-0" action={{ altText: "Transfer ownership", onClick: () => {}, label: "Transfer", }} /> </ToastProvider>)
export function ToastErrorWithActionExample() { const { toast } = useToast()
return ( <> <Toaster /> <Button onClick={() => toast({ title: "Error", description: "Are you sure you want to transfer ownership to Max?", variant: "error", action: { altText: "Transfer ownership", onClick: () => {}, label: "Transfer", }, }) } > Create toast </Button> </> )}
API Reference: Toast
This component uses the Radix UI API.
- variant
- Set a predefined look.
"info" | "success" | "warning" | "error" | "loading"
Default: "info"
- title
- Set title.
string
- description
- Set a description.
any
- action
- Set an action item.
- label: string
- altText: string
- onClick: () => void | Promise<void>
ActionProps
- disableDismiss
- Remove the default close button.
boolean
Default: false
API Reference: ToastProvider
This component uses the Radix UI API.
API Reference: ToastViewport
This component uses the Radix UI API.