Components
Toast
A collection of toast notification components
Component Source'use client'import { useRef } from 'react'import { Button } from '@/components/ui/button'import { anchoredToast, AnchoredToastProvider, StackedToastProvider, toast,} from '@/components/ui/toast'export default function ToastDemo() { return ( <StackedToastProvider> <AnchoredToastProvider> <ToastDemoContent /> <hr className='my-8 w-full border-t' /> <ToastAnchorDemoContent /> </AnchoredToastProvider> </StackedToastProvider> )}function ToastDemoContent() { 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( new Promise((resolve) => setTimeout(() => resolve('Data loaded'), 3000), ), { loading: 'Loading...', 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', 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> )}function ToastAnchorDemoContent() { const anchorRef = useRef<HTMLButtonElement>(null) return ( <div className='flex flex-col items-center justify-center gap-4'> <Button ref={anchorRef} variant='outline'> Anchor Toasts Here </Button> <div className='flex flex-wrap items-center justify-center gap-4'> <Button variant='outline' onClick={() => anchoredToast.add({ title: 'This toast is anchored to the button.', description: 'It appears next to the button when triggered.', type: 'error', positionerProps: { anchor: anchorRef.current }, }) } > Show Anchored Toast </Button> </div> </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()const anchoredToast = 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 StackedToastProvider({ position = 'bottom-right', children, ...props}: ToastProviderProps) { return ( <ToastPrimitive.Provider toastManager={toast} {...props}> {children} <Toaster position={position} /> </ToastPrimitive.Provider> )}function Toaster({ position = 'bottom-right',}: Pick<ToastProviderProps, 'position'>) { const { toasts } = ToastPrimitive.useToastManager() let 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 ( <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)', )} > {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(9999-var(--toast-index))] h-(--toast-calc-height) w-full rounded-lg border bg-popover text-popover-foreground shadow-lg/5 select-none', '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', '[transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s]', 'before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]', // Base positioning using data-position 'data-[position*=right]:right-0 data-[position*=right]:left-auto', 'data-[position*=left]:right-auto data-[position*=left]:left-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', // Gap fill for hover 'after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full', 'data-[position*=top]:after:top-full', 'data-[position*=bottom]:after:bottom-full', // Define some variables '[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]', // Define offset-y variable '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))]', // Default state transform 'data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]', 'data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]', // Limited state 'data-limited:opacity-0', // Expanded state 'data-expanded:h-(--toast-height)', 'data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]', // Starting and ending animations 'data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]', 'data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]', 'data-ending-style:opacity-0', // Ending animations (direction-aware) 'data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]', 'data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]', 'data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]', 'data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]', 'data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]', // Ending animations (expanded) 'data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]', 'data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]', 'data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]', 'data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]', )} swipeDirection={swipeDirection} > <ToastPrimitive.Content data-slot='toast-content' className='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={cn('flex flex-1', { 'flex-col gap-1': toast.title, 'flex-row gap-2': !toast.title, })} > <div data-slot='toast-header' className='flex 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> ) })} </ToastPrimitive.Viewport> </ToastPrimitive.Portal> )}function AnchoredToastProvider({ children, ...props }: ToastProviderProps) { return ( <ToastPrimitive.Provider toastManager={anchoredToast} {...props}> {children} <AnchoredToaster /> </ToastPrimitive.Provider> )}function AnchoredToaster() { const { toasts } = ToastPrimitive.useToastManager() return ( <ToastPrimitive.Portal data-slot='anchored-toaster-portal'> <ToastPrimitive.Viewport className='outline-none' data-slot='anchored-toast-viewport' > {toasts.map((toast) => { const positionerProps = toast.positionerProps if (!positionerProps?.anchor) return null const Icon = toast.type ? icons[toast.type as keyof typeof icons] : null const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false return ( <ToastPrimitive.Positioner key={toast.id} data-slot='anchored-toast-positioner' sideOffset={positionerProps.sideOffset ?? 4} toast={toast} className='z-50 max-w-[min(--spacing(64),var(--available-width))]' > <ToastPrimitive.Root data-slot='anchored-toast-root' className={cn( 'border bg-popover text-popover-foreground shadow-lg/5', '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', 'relative transition-[scale,opacity] not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]', tooltipStyle ? 'rounded-md before:rounded-[calc(var(--radius-md)-1px)]' : 'rounded-lg before:rounded-[calc(var(--radius-lg)-1px)]', )} toast={toast} > {tooltipStyle ? ( <ToastPrimitive.Content data-slot='anchored-toast-tooltip-content' className='pointer-events-auto px-2 py-1' > <ToastPrimitive.Title data-slot='anchored-toast-title' /> </ToastPrimitive.Content> ) : ( <ToastPrimitive.Content data-slot='anchored-toast-content' className='flex items-center justify-between gap-0.5 px-2 py-3 transition-opacity data-behind:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100' > <div data-slot='anchored-toast-message' className={cn('flex flex-1', { 'flex-col gap-1': toast.title, 'flex-row gap-2': !toast.title, })} > <div data-slot='anchored-toast-header' className='flex items-center gap-2' > {Icon && ( <Icon data-slot='anchored-toast-icon' className='size-4 shrink-0 in-data-[type=loading]:animate-spin in-data-[type=loading]:text-muted-foreground' /> )} <ToastPrimitive.Title data-slot='anchored-toast-title' className='text-xs font-medium' /> </div> <ToastPrimitive.Description data-slot='anchored-toast-description' className='text-xs not-in-data-type:text-muted-foreground in-data-[type=loading]:text-muted-foreground' /> </div> {toast.actionProps && ( <ToastPrimitive.Action data-slot='anchored-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> </ToastPrimitive.Positioner> ) })} </ToastPrimitive.Viewport> </ToastPrimitive.Portal> )}export { toast, anchoredToast, StackedToastProvider, AnchoredToastProvider }