Beta

UI

Toast

A lightweight notification component.

    Installation

    1. 1

      Install dependencies:

      npm install @radix-ui/react-toast @remixicon/react
    2. 2

      Add Toast.tsx component:

      Copy and paste the code into your project’s component directory. Do not forget to update the import paths.
      // Tremor Raw Toast [v0.0.1]
      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?: any  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 = null
          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,        )}        {...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,}
      
    3. 3

      Add Toaster.tsx component:

      Copy and paste the code into your project’s component directory. Do not forget to update the import paths.
      // Tremor Raw 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 }
      
    4. 4

      Add useToast.ts hook:

      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 }
      
      
    5. 5

      Update tailwind.config.ts

      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

      Example: Toast with variant warning and custom duration

        Example: Toast with variant Loading and custom duration

          Example: Toast with variant Success and custom duration

            Example: Toast with variant Error and custom duration

              Example: Toast with variant Error and action

                API Reference: Toast

                This component uses the Radix UI API.

                variant
                "info" | "success" | "warning" | "error" | "loading"
                Set a predefined look.

                Default: "info"

                title
                string
                Set title.
                description
                any
                Set a description.
                action
                ActionProps
                Set an action item.
                • label: string
                • altText: string
                • onClick: () => void | Promise<void>
                disableDismiss
                boolean
                Remove the default close button.

                Default: false

                API Reference: ToastProvider

                This component uses the Radix UI API.

                API Reference: ToastViewport

                This component uses the Radix UI API.