Component
Toast

Toast

A succinct message that is displayed temporarily.

Installation

Install the following dependencies

npm i @radix-ui/react-toast

Copy and paste the following code into your project

components/ui/toast/index.tsx
'use client'
 
import * as React from 'react'
import * as ToastPrimitive from '@radix-ui/react-toast'
import { X } from 'lucide-react'
import { createStyleContext } from '@shadow-panda/style-context'
import { styled } from '@shadow-panda/styled-system/jsx'
import { toast, toastViewport, icon } from '@shadow-panda/styled-system/recipes'
 
const { withProvider, withContext } = createStyleContext(toast)
 
export const ToastProvider = ToastPrimitive.Provider
export const ToastViewport = styled(ToastPrimitive.Viewport, toastViewport)
export const Toast = withProvider(styled(ToastPrimitive.Root), 'root', { className: 'group' })
export const ToastAction = withContext(styled(ToastPrimitive.Action), 'action')
export const ToastClose = withContext(styled(ToastPrimitive.Close), 'close', {
  children: <X className={icon()} />,
})
export const ToastTitle = withContext(styled(ToastPrimitive.Title), 'title')
export const ToastDescription = withContext(styled(ToastPrimitive.Description), 'description')
 
export type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
export type ToastActionElement = React.ReactElement<typeof ToastAction>
components/ui/toast/toaster.tsx
'use client'
 
import {
  Toast,
  ToastClose,
  ToastDescription,
  ToastProvider,
  ToastTitle,
  ToastViewport,
} from '@/components/ui/toast'
import { useToast } from '@/components/ui/toast/use-toast'
 
export function Toaster() {
  const { toasts } = useToast()
 
  return (
    <ToastProvider>
      {toasts.map(({ id, title, description, action, ...props }) => (
        <Toast key={id} {...props}>
          <div className="grid gap-1">
            {title && <ToastTitle>{title}</ToastTitle>}
            {description && <ToastDescription>{description}</ToastDescription>}
          </div>
          {action}
          <ToastClose />
        </Toast>
      ))}
      <ToastViewport />
    </ToastProvider>
  )
}
components/ui/toast/use-toast.tsx
import * as React from 'react'
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
 
const TOAST_LIMIT = 1
const 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
 
      // ! Side effects ! - This could be extracted into a dismissToast() action,
      // but I'll keep it here for simplicity
      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: [] }
 
function dispatch(action: Action) {
  memoryState = reducer(memoryState, action)
  listeners.forEach((listener) => {
    listener(memoryState)
  })
}
 
type Toast = Omit<ToasterToast, 'id'>
 
export function toast({ ...props }: Toast) {
  const id = genId()
 
  const update = (props: ToasterToast) =>
    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: boolean) => {
        if (!open) dismiss()
      },
    },
  })
 
  return {
    id: id,
    dismiss,
    update,
  }
}
 
export 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 }),
  }
}

Update the import paths to match your project setup

Add the Toaster component to your app

app/layout.tsx
import { Toaster } from '@/components/ui/toast/toaster'
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <main>{children}</main>
        <Toaster />
      </body>
    </html>
  )
}

Edit panda.config.ts to load the toast variant css

panda.config.ts
import { defineConfig } from '@pandacss/dev'
 
export default defineConfig({
  // ...rest of your config
 
  staticCss: {
    recipes: {
      // Load toast variant styles since it cannot be statically analyzed
      toast: [{ variant: ['*'] }],
    },
  },
})

Usage

import { useToast } from '@/components/ui/toast/use-toast'
export const Example = () => {
  const { toast } = useToast()
 
  return (
    <Button
      onClick={() => {
        toast({
          title: 'Scheduled: Catch up',
          description: 'Friday, February 10, 2023 at 5:57 PM',
        })
      }}
    >
      Show Toast
    </Button>
  )
}

Examples

Simple

With Title

With Action

Destructive

Use toast({ variant: "destructive" }) to display a destructive toast.