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>
)
}
ℹ️
To display multiple toasts at the same time, you can update the TOAST_LIMIT
in use-toast.tsx
.
Examples
Simple
With Title
With Action
Destructive
Use toast({ variant: "destructive" })
to display a destructive toast.