Components
Toast
A collection of toast notification components
Component Source'use client'import { Button } from '@/components/ui/button'import { toast } from '@/components/ui/toast'export default function ToastDemo() { const toasts = [ { label: 'Show Simple Toast', action: () => toast.add({ title: 'This is a simple toast notification.' }), }, { label: 'Show Success Toast', action: () => toast.add({ title: 'This is a toast notification!', type: 'success' }), }, { label: 'Show Error Toast', action: () => toast.add({ title: 'This is a toast notification!', type: 'error' }), }, { label: 'Show Info Toast', action: () => toast.add({ title: 'This is a toast notification!', type: 'info' }), }, { label: 'Show Warning Toast', action: () => toast.add({ title: 'This is a toast notification!', type: 'warning' }), }, { label: 'Show Promise Toast', action: () => toast.promise( // oxlint-disable-next-line promise/avoid-new new Promise((resolve) => // oxlint-disable-next-line no-promise-executor-return setTimeout(() => resolve('Data loaded'), 3000) ), { loading: 'Loading data...', success: (data) => `Success: ${data}`, error: (err) => `Error: ${err}`, } ), }, { label: 'Show Toast with Action', action: () => toast.add({ title: 'Custom Action', description: 'This toast has a custom action button.', actionProps: { children: 'Undo', // oxlint-disable-next-line no-alert onClick: () => alert('Undo action clicked!'), }, }), }, ] return ( <div className='flex flex-wrap items-center justify-center gap-4'> {toasts.map(({ label, action }) => ( <Button key={label} variant='outline' onClick={action}> {label} </Button> ))} </div> )}Installation
CLI
npx shadcn add https://yuki-ui.vercel.app/r/toast.jsonnpx shadcn add https://yuki-ui.vercel.app/r/toast.jsonpnpm dlx shadcn add https://yuki-ui.vercel.app/r/toast.jsonbunx --bun shadcn add https://yuki-ui.vercel.app/r/toast.jsonManual
Copy and paste the following code into your project.
'use client'import { Toast as ToastPrimitive } from '@base-ui/react'import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon,} from 'lucide-react'import { buttonVariants } from '@/components/ui/button'import { cn } from '@/lib/utils'const toast = ToastPrimitive.createToastManager()type ToastPosition = | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'const icons = { success: CircleCheckIcon, info: InfoIcon, warning: TriangleAlertIcon, error: OctagonXIcon, loading: Loader2Icon,} as constinterface ToastProviderProps extends ToastPrimitive.Provider.Props { position?: ToastPosition}function ToastProvider({ position = 'bottom-right', children, ...props}: ToastProviderProps) { return ( <ToastPrimitive.Provider toastManager={toast} {...props}> {children} <ToastPrimitive.Portal data-slot='toaster-portal'> <ToastPrimitive.Viewport data-slot='toaster-viewport' data-position={position} className={cn( 'fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]', // Vertical positions 'data-[position*=top]:top-(--toast-inset)', 'data-[position*=bottom]:bottom-(--toast-inset)', // Horizontal positions 'data-[position*=left]:left-(--toast-inset)', 'data-[position*=center]:left-1/2 data-[position*=center]:-translate-x-1/2', 'data-[position*=right]:right-(--toast-inset)' )} > <Toaster position={position} /> </ToastPrimitive.Viewport> </ToastPrimitive.Portal> </ToastPrimitive.Provider> )}function Toaster({ position = 'bottom-right',}: Pick<ToastProviderProps, 'position'>) { const { toasts } = ToastPrimitive.useToastManager() const swipeDirection: ToastPrimitive.Root.Props['swipeDirection'] = [] if (position.includes('top')) swipeDirection.push('up') if (position.includes('bottom')) swipeDirection.push('down') if (position.includes('left')) swipeDirection.push('left') if (position.includes('right')) swipeDirection.push('right') if (position.includes('center')) swipeDirection.push('right', 'left') return toasts.map((_toast) => { const Icon = _toast.type ? icons[_toast.type as keyof typeof icons] : null return ( <ToastPrimitive.Root key={_toast.id} data-slot='toast-root' data-position={position} data-type={_toast.type} toast={_toast} className={cn( 'group/toast absolute z-[calc(1000-var(--toast-index))] h-(--toast-calc-height) w-full rounded-lg bg-clip-padding shadow-lg/5 select-none [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]', // Variables for calculating position and animation '[--gap:0.5rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]', 'data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]', 'data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]', // Variants 'border bg-popover text-popover-foreground', 'data-[type=success]:border-green-600 data-[type=success]:bg-green-100 data-[type=success]:text-green-600 dark:data-[type=success]:border-green-400 dark:data-[type=success]:bg-green-950 dark:data-[type=success]:text-green-400', 'data-[type=error]:border-red-600 data-[type=error]:bg-red-100 data-[type=error]:text-red-600 dark:data-[type=error]:border-red-400 dark:data-[type=error]:bg-red-950 dark:data-[type=error]:text-red-400', 'data-[type=info]:border-blue-600 data-[type=info]:bg-blue-100 data-[type=info]:text-blue-600 dark:data-[type=info]:border-blue-400 dark:data-[type=info]:bg-blue-950 dark:data-[type=info]:text-blue-400', 'data-[type=warning]:border-yellow-600 data-[type=warning]:bg-yellow-100 data-[type=warning]:text-yellow-600 dark:data-[type=warning]:border-yellow-400 dark:data-[type=warning]:bg-yellow-950 dark:data-[type=warning]:text-yellow-400', // Base positioning using data-position 'data-[position*=right]:right-0 data-[position*=right]:left-auto data-[position*=right]:mr-0', 'data-[position*=left]:right-auto data-[position*=left]:left-0 data-[position*=left]:ml-0', 'data-[position*=center]:right-0 data-[position*=center]:left-0', 'data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top', 'data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom', // Swipe animations 'data-[position*=bottom]:data-[starting-style]:[transform:translateY(150%)] data-[position*=top]:data-[starting-style]:[transform:translateY(-150%)]', 'transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]', 'data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))]', // After swipe animations 'after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[""]', // Ending styles 'data-[ending-style]:opacity-0 data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]', 'data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]', '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]', // Expanded styles 'data-[expanded]:h-[var(--toast-height)] data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[position*=top]:data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)*-1))]', 'data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]' )} swipeDirection={swipeDirection} > <ToastPrimitive.Content data-slot='toast-content' className='relative flex items-center justify-between gap-0.5 px-4 py-3 transition-opacity data-behind:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100' > <div data-slot='toast-message' className='@container/toast-content grid flex-1 auto-rows-min items-start gap-1 not-has-data-[slot=toast-title]:flex not-has-data-[slot=toast-title]:items-center has-data-[slot=toast-action]:grid-cols-[1fr_auto] has-data-[slot=toast-description]:grid-rows-[auto_auto]' > <div data-slot='toast-header' className='flex flex-row items-center gap-2' > {Icon && ( <Icon data-slot='toast-icon' className='size-4 shrink-0 in-data-[type=loading]:animate-spin in-data-[type=loading]:text-muted-foreground' /> )} <ToastPrimitive.Title data-slot='toast-title' className='text-sm font-medium' /> </div> <ToastPrimitive.Description data-slot='toast-description' className='text-sm not-in-data-type:text-muted-foreground in-data-[type=loading]:text-muted-foreground' /> </div> {_toast.actionProps && ( <ToastPrimitive.Action data-slot='toast-action' className={cn( buttonVariants({ variant: 'ghost', size: 'xs' }), 'hover:bg-current/20 hover:text-current dark:hover:bg-current/20' )} {..._toast.actionProps} /> )} </ToastPrimitive.Content> </ToastPrimitive.Root> ) })}export { toast, ToastProvider }