Toast
Toast notifications built on Base UI with a simple imperative API.
A lightweight toast notification system built on top of @base-ui/react/toast.
The component exposes a global toast manager with convenience methods for
default, success, error, info, warning, and promise-based notifications.
Rendering is enabled by ToastProvider.
Installation
CLI
npx shadcn add https://ui.tiesen.id.vn/r/toast.jsonManual
Copy and paste the following code into your project.
'use client'import type { ToastManagerAddOptions } from '@base-ui/react/toast'import { Toast as ToastPrimitive } from '@base-ui/react/toast'import { AlertCircleIcon, AlertTriangleIcon, CheckCircleIcon, InfoIcon, Loader2Icon,} from 'lucide-react'import * as React from 'react'import { cn } from '@/lib/utils'const toastManager = ToastPrimitive.createToastManager()type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'const ICONS = { loading: Loader2Icon, success: CheckCircleIcon, error: AlertCircleIcon, info: InfoIcon, warning: AlertTriangleIcon,} as constfunction ToastList({ position = 'bottom-right',}: Readonly<{ position?: ToastPosition }>) { const { toasts } = ToastPrimitive.useToastManager() const swipeDirection = React.useMemo(() => { const directions: ('up' | 'down' | 'left' | 'right')[] = [] if (!position) return directions if (position.includes('top')) directions.push('up') if (position.includes('bottom')) directions.push('down') if (position.includes('left')) directions.push('left') if (position.includes('right')) directions.push('right') return directions }, [position]) return toasts.map((toast) => { const Icon = toast.type ? ICONS[toast.type as keyof typeof ICONS] : undefined return ( <ToastPrimitive.Root key={toast.id} toast={toast} data-slot='toast' data-position={position} data-type={toast.type ?? 'default'} swipeDirection={swipeDirection} className={cn( 'group/toast z-[calc(1000-var(--toast-index))] [--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))]', 'absolute h-(--height) w-full rounded-md border bg-popover text-popover-foreground shadow-sm select-none [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] data-expanded:h-(--toast-height)', "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-ending-style:opacity-0 data-limited:opacity-0", /* Global Base Transforms theo Top vs Bottom */ 'data-[position^=top]:transform-[translateX(calc(var(--toast-swipe-movement-x)+var(--x-offset,0px)))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]', 'data-[position^=bottom]:transform-[translateX(calc(var(--toast-swipe-movement-x)+var(--x-offset,0px)))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]', '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', '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', /* Global Expanded Styles */ 'data-[position^=top]:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)*-1))]', 'data-[position^=bottom]:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))]', /* Global Ending/Starting Styles */ 'data-[position^=top]:data-starting-style:transform-[translateY(-150%)]', 'data-[position^=top]:[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(-150%)]', 'data-[position^=bottom]:data-starting-style:transform-[translateY(150%)]', 'data-[position^=bottom]:[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]', /* Global Swipe / Ending Directions */ 'data-[position^=top]:data-ending-style:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))]', 'data-[position^=bottom]:data-ending-style:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))]', 'data-[position*=left]:data-ending-style:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]', 'data-[position*=right]:data-ending-style:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]', /* Variants */ 'data-[type=success]:border-green-600 data-[type=success]:bg-[color-mix(in_oklab,var(--color-green-600)_10%,var(--color-popover))] data-[type=success]:text-green-600 dark:data-[type=success]:border-green-400 dark:data-[type=success]:bg-[color-mix(in_oklab,var(--color-green-400)_10%,var(--color-popover))] dark:data-[type=success]:text-green-400', 'data-[type=error]:border-red-600 data-[type=error]:bg-[color-mix(in_oklab,var(--color-red-600)_10%,var(--color-popover))] data-[type=error]:text-red-600 dark:data-[type=error]:border-red-400 dark:data-[type=error]:bg-[color-mix(in_oklab,var(--color-red-400)_10%,var(--color-popover))] dark:data-[type=error]:text-red-400', 'data-[type=info]:border-blue-600 data-[type=info]:bg-[color-mix(in_oklab,var(--color-blue-600)_10%,var(--color-popover))] data-[type=info]:text-blue-600 dark:data-[type=info]:border-blue-400 dark:data-[type=info]:bg-[color-mix(in_oklab,var(--color-blue-400)_10%,var(--color-popover))] dark:data-[type=info]:text-blue-400', 'data-[type=warning]:border-yellow-600 data-[type=warning]:bg-[color-mix(in_oklab,var(--color-yellow-600)_10%,var(--color-popover))] data-[type=warning]:text-yellow-600 dark:data-[type=warning]:border-yellow-400 dark:data-[type=warning]:bg-[color-mix(in_oklab,var(--color-yellow-400)_10%,var(--color-popover))] dark:data-[type=warning]:text-yellow-400' )} > <ToastPrimitive.Content data-slot='toast-content' className='flex flex-row items-center gap-2 overflow-hidden p-3' > {Icon && ( <Icon className='size-4 shrink-0 text-current group-data-[type=loading]/toast:animate-spin' aria-hidden='true' /> )} <div className='flex flex-1 flex-col gap-1 overflow-hidden'> <ToastPrimitive.Title data-slot='toast-title' className='text-sm leading-snug font-medium' /> <ToastPrimitive.Description data-slot='toast-description' className='text-xs text-current/80' /> </div> <ToastPrimitive.Action data-slot='toast-action' className='inline-flex h-6 shrink-0 items-center justify-center gap-1 rounded-md bg-current/10 bg-clip-padding px-2 text-xs font-medium whitespace-nowrap outline-none select-none hover:bg-current/20' /> </ToastPrimitive.Content> </ToastPrimitive.Root> ) })}function ToastProvider({ position = 'bottom-right', timeout = 5000, limit = 3, children,}: Omit< React.ComponentProps<typeof ToastPrimitive.Provider>, 'toastManager'> & { position?: ToastPosition }) { return ( <ToastPrimitive.Provider toastManager={toastManager} timeout={timeout} limit={limit} > {children} <ToastPrimitive.Portal data-slot='toast-portal'> <ToastPrimitive.Viewport data-slot='toast-viewport' data-position={position} className={cn( 'fixed z-9999 mx-auto w-[calc(100vw-var(--toast-inset)*2)] [--toast-inset:--spacing(4)] sm:w-90', 'data-[position^=top]:top-(--toast-inset) data-[position^=top]:bottom-auto', 'data-[position^=bottom]:top-auto data-[position^=bottom]:bottom-(--toast-inset)', 'data-[position*=left]:left-(--toast-inset)', 'data-[position*=right]:right-(--toast-inset)' )} > <ToastList position={position} /> </ToastPrimitive.Viewport> </ToastPrimitive.Portal> </ToastPrimitive.Provider> )}const useToast = ToastPrimitive.useToastManagerconst toast = { ...toastManager, show: ( title: React.ReactNode, options?: Omit<ToastManagerAddOptions<object>, 'title'> ) => toastManager.add({ type: 'default', title, ...options }), success: ( title: React.ReactNode, options?: Omit<ToastManagerAddOptions<object>, 'title'> ) => toastManager.add({ type: 'success', title, ...options }), error: ( title: React.ReactNode, options?: Omit<ToastManagerAddOptions<object>, 'title'> ) => toastManager.add({ type: 'error', title, ...options }), info: ( title: React.ReactNode, options?: Omit<ToastManagerAddOptions<object>, 'title'> ) => toastManager.add({ type: 'info', title, ...options }), warning: ( title: React.ReactNode, options?: Omit<ToastManagerAddOptions<object>, 'title'> ) => toastManager.add({ type: 'warning', title, ...options }),}export { ToastProvider, toast, useToast }Basic setup
Wrap the application with ToastProvider:
import { ToastProvider } from '@/registry/ui/toast'
export function App() {
return (
<ToastProvider>
<YourApp />
</ToastProvider>
)
}ToastProvider creates the Base UI toast viewport and renders active toasts.
The defaults are bottom-right, 5000ms, and a visible limit of 3.
Usage
import { toast } from '@/registry/ui/toast'
toast.show('This is a simple toast.')
toast.success('Saved successfully!')
toast.error('Something went wrong.')
toast.info('Your account has been updated.')
toast.warning('This action may have side effects.')Each convenience method accepts a React node as the title and optional Base UI toast options.
Toast types
The renderer supports:
defaultloadingsuccesserrorinfowarning
The loading, success, error, info, and warning types have Lucide
icons; the loading icon is animated.
Custom actions
Use the underlying manager API when you need an action button:
toast.add({
title: 'Custom Action',
description: 'This toast has a custom action button.',
actionProps: {
children: 'Undo',
onClick: () => toast.close(),
},
})Promise toasts
toast.promise() displays loading, success, and error states around a promise:
toast.promise(fetchData(), {
loading: 'Loading data...',
success: (data) => `Loaded: ${data}`,
error: (error) => String(error),
})Example:
toast.promise(
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) resolve('Data loaded successfully!')
else reject('Failed to load data.')
}, 2000)
),
{
loading: 'Loading data...',
success: (data) => data as string,
error: (error) => error as string,
}
)Position
ToastProvider supports:
type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'Example:
<ToastProvider position='top-right'>
<App />
</ToastProvider>The position controls viewport placement and the swipe directions used by the toast list.
Timeout and limit
<ToastProvider timeout={8000} limit={5}>
<App />
</ToastProvider>These values are passed directly to Base UI's toast provider.
Using the toast manager
import { useToast } from '@/registry/ui/toast'
function MyComponent() {
const toastManager = useToast()
// Use the Base UI toast manager API here.
}useToast is an alias for ToastPrimitive.useToastManager. The exported
toast object also spreads the same global manager, exposing Base UI methods
such as add, close, and promise.
Accessibility
Toast content uses Base UI's toast primitives:
ToastPrimitive.RootToastPrimitive.ContentToastPrimitive.TitleToastPrimitive.DescriptionToastPrimitive.Action
The decorative icon is marked aria-hidden.
Styling
The toast root exposes:
data-slot="toast" data-position="..." data-type="..."Tailwind classes use these attributes for positioning, animation, lifecycle states, swipe behavior, and visual variants.
The built-in variants use green, red, blue, and yellow accents for success, error, info, and warning, with dark-mode variants included.
API reference
ToastProvider
<ToastProvider position='bottom-right' timeout={5000} limit={3}>
{children}
</ToastProvider>| Prop | Default | Description |
|---|---|---|
position | "bottom-right" | Toast viewport position. |
timeout | 5000 | Default timeout passed to Base UI. |
limit | 3 | Maximum number of visible toasts. |
toast
toast.show(title, options?)
toast.success(title, options?)
toast.error(title, options?)
toast.info(title, options?)
toast.warning(title, options?)title is a React.ReactNode. options is derived from
ToastManagerAddOptions<object> with title omitted.
Because toast spreads the underlying manager, its Base UI manager methods are
also available.
useToast
const toastManager = useToast()This is an alias of ToastPrimitive.useToastManager.