Skip to Content

Toast

A notification system built on Base UI’s Toast primitive. Toasts slide in from the bottom corner at the inline end (bottom-right in left-to-right layouts, bottom-left in right-to-left ones) and auto-dismiss. In both directions a toast can be swiped away down, left or right, and it leaves the way it was swiped. The useToast hook triggers them from anywhere within the provider.

Import

import { Toast } from "chunks-ui"

Setup

Wrap your app (or the relevant subtree) with Toast.Provider and place Toast.Viewport where the toasts should render:

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <Toast.Provider> {children} <Toast.Viewport /> </Toast.Provider> ) }

Basic Usage

Use Toast.useToast() to get the add function and trigger notifications:

function SaveButton() { const { add } = Toast.useToast() return ( <button onClick={() => add({ title: "Saved", description: "Your changes have been saved." })}> Save </button> ) }

Per-toast Styling

chunks-ui doesn’t ship any icons, semantic types, or default colours — bring your own. Three per-toast slots let you plug into whatever icon library and design tokens your project uses:

FieldPurpose
iconComponent rendered in the leading slot. Receives a className prop pre-populated with default sizing (size-5 shrink-0 self-start).
iconClassNameExtra classes merged onto icon — use for color.
classNameExtra classes merged onto Toast.Root — use for accent bars, borders, background tints.

For the simple case, pass a lucide component directly:

import { CheckCircle2 } from 'lucide-react' add({ title: "File saved", icon: CheckCircle2, iconClassName: "text-success-text", })

Semantic Types — A Custom Pattern

The example below renders four colour-coded variants — but chunks-ui doesn’t ship semantic types. The icons, accent borders, and text colours are entirely a consumer-side pattern built on the three slots above.

Define a pure-data TYPE_STYLES map once and spread it into add() at the call site:

import { Button, Toast } from 'chunks-ui' import { AlertTriangle, CheckCircle2, Info, XCircle } from 'lucide-react' const TYPE_STYLES = { primary: { icon: Info, iconClassName: 'text-primary-text', className: 'border-s-4 border-primary' }, success: { icon: CheckCircle2, iconClassName: 'text-success-text', className: 'border-s-4 border-success' }, destructive: { icon: XCircle, iconClassName: 'text-destructive-text', className: 'border-s-4 border-destructive' }, warning: { icon: AlertTriangle, iconClassName: 'text-warning-text', className: 'border-s-4 border-warning' }, } as const type ToastType = keyof typeof TYPE_STYLES function ToastTrigger({ label, title, description, type, }: { label: string title: string description?: string type?: ToastType }) { const { add } = Toast.useToast() return ( <Button type="button" variant="outlined" color={type} onClick={() => add({ title, description, ...(type && TYPE_STYLES[type]) })} > {label} </Button> ) }

No factory functions, no closures — the icon references are stable lucide components, so React reconciles them cleanly across re-renders. Add a type, drop a row in the map; rename a colour, change one row. The library never has to ship an opinion.

With Action

Pass actionProps to render a labelled action button inside the toast:

const { add } = Toast.useToast() add({ title: "Message deleted", description: "The message has been removed from your inbox.", actionProps: { children: "Undo", onClick: () => restoreMessage(), }, })

High Priority

Pass priority: "high" for a toast the user must not miss. Screen readers announce it at once, interrupting what they read; a toast of the default low priority waits until they finish:

add({ title: "Connection lost", description: "Trying to reconnect…", priority: "high", })

Base UI 1.9 hides a high-priority toast from screen readers until keyboard focus enters the viewport, and Tab passes over the toast for that time: press F6, then Tab, to reach it. See the note under Props.

Dismissible Toasts

By default toasts auto-dismiss after the provider’s timeout and don’t render a close button. Opt in per-toast by passing dismissible: true to add():

const { add } = Toast.useToast() add({ title: "Saved", description: "Your changes are safe.", dismissible: true, })

That toast gets a × button in its top-end corner (top-right in left-to-right layouts, top-left in right-to-left ones) that dismisses it via Base UI’s Toast.Close. Other toasts in the same viewport are unaffected — dismissibility is a per-toast concern, not a viewport-wide setting.

closeProps takes the props of that button, as actionProps does for the action: className merges after the library classes, and aria-label replaces labels.close for that toast. Unlike actionProps, it does not render the button on its own: the button appears with dismissible or onClose. To keep the focus ring flush with the button instead of 2px outside it:

add({ title: "Saved", dismissible: true, closeProps: { className: "focus-visible:outline-offset-0" }, })

Note: dissmissable is a deprecated misspelled alias of dismissible. When both are set, dismissible wins.

Reacting to Close

If you need to run a side effect when a toast closes, such as tracking analytics, offering an undo stack or syncing with a backend, pass onClose. It takes no arguments and fires once per close, whichever way the toast closes: the close button, the timeout, a swipe, Escape with focus inside the toast, or manager.close. Passing onClose also implicitly enables the close button, so you don’t need to also set dismissible. To track a toast that closes by itself without showing a button, set dismissible: false. onClose doesn’t say which toast closed; to know that, keep the id that add() returns:

const id = add({ title: "Message archived", onClose: () => { analytics.track('toast_closed', { id }) }, })

onClose runs in addition to Base UI’s built-in dismissal. It is for side effects, not for preventing the toast from closing. To keep a toast open on a click of the close button, call event.preventBaseUIHandler() in closeProps.onClick: the toast stays and onClose does not fire. Set timeout: 0 as well, or the timer closes the toast anyway:

add({ title: "Unsaved changes", timeout: 0, dismissible: true, closeProps: { onClick: (event) => { if (hasUnsaved) event.preventBaseUIHandler() }, }, })

Promise Toasts

toast.promise(promise, { loading, success, error }) wires a single toast through the lifecycle of an async operation. It starts with the loading options, then swaps to success or error once the promise settles. success and error can be static options or functions that receive the resolved value / rejection reason:

import { CheckCircle2, XCircle } from 'lucide-react' const { promise } = Toast.useToast() promise(uploadFile(), { loading: { title: "Uploading file…", description: "Hold tight." }, success: (result) => ({ title: "Upload complete", description: `Saved ${result.name} to your workspace.`, icon: CheckCircle2, iconClassName: "text-success-text", }), error: (err) => ({ title: "Upload failed", description: err.message, icon: XCircle, iconClassName: "text-destructive-text", }), })

The method returns the original promise, so you can still await or .catch() it downstream — the toast is a side effect, not a replacement for your own error handling.

Stacking Behaviour

By default Toast.Viewport renders toasts as a flex column. For the “stacked deck” pattern — where a pile of notifications collapses behind the frontmost one and fans out on hover — chunks-ui ships two opt-in CSS classes in theme.css:

ClassWhereWhat it does
toast-stack-viewportToast.ViewportOverrides the default flex layout so children can absolute-stack at the same anchor point.
toast-stackeach toast’s classNameDrives the collapsed/expanded transforms off Base UI’s per-root CSS variables (--toast-index, --toast-height, --toast-frontmost-height, --toast-offset-y, --toast-swipe-movement-x/y) and responds to data-expanded, data-starting-style, data-ending-style, and data-swipe-direction.
<Toast.Provider limit={5}> <TriggerButton /> <Toast.Viewport className="toast-stack-viewport" /> </Toast.Provider>

Then give each toast the toast-stack class when you add it:

add({ title: "Notification", description: "Hover the stack to expand.", className: "toast-stack", })

The stacking CSS respects prefers-reduced-motion: reduce — users with that setting get an instant stack/expand with no transitions. The pattern is ported directly from Base UI’s reference example, so the visual and physics match what Base UI documents.

Compound Parts

PartDescription
Toast.ProviderContext provider. Must wrap any component that calls useToast. Accepts timeout and limit (default 3). In the default viewport, toasts past the limit stay hidden until newer ones close, then slide in (at once with reduced motion, or in browsers without @starting-style).
Toast.ViewportFixed-position container that renders all active toasts.
Toast.RootIndividual toast wrapper with slide/fade enter/exit transitions.
Toast.TitleBold toast heading.
Toast.DescriptionSupporting text in text-muted-foreground.
Toast.CloseIcon button that dismisses the toast. Children you pass replace the X; with a render element and no children, there is no X. The button keeps its aria-label whatever its content (labels.close on the button a toast adds itself; “Close” on one you render yourself): if the children are visible text, pass an aria-label that contains it.
Toast.ActionOptional action button rendered inside the toast.
Toast.useToastHook that returns { toasts, add, close, update, promise } for the nearest provider.

Props

Per-toast options — add / update / promise

Pass any subset of these at the call site. The fields below are chunks-ui additions on top of Base UI’s ToastObject — all other options (title, description, type, timeout, actionProps, priority, data, …) are forwarded directly to Base UI; see the ToastObject API reference for the full list.

NameTypeDefault
iconComponentType<{ className?: string; }>

Icon component rendered in the toast’s leading slot. Receives a className prop pre-populated with default sizing (size-5 shrink-0 self-start), merged with iconClassName if provided. Pass a lucide/heroicons component directly, or a wrapper for fully custom rendering.

iconClassNamestring

Extra classes applied to the rendered icon. Use this for color (e.g. text-success-text) without having to wrap the icon component.

classNamestring

Extra classes merged into Toast.Root’s className. Use this for per-toast styling like accent bars, borders, or background tints — typically driven off type.

dismissibleboolean

When true, renders a close (×) button on this toast. Clicking it dismisses the toast via Base UI’s Toast.Close. Passing onClose implicitly enables this, so you only need to set dismissible when you want the button without a side-effect handler. dismissible: false hides the button even when onClose is set.

false
onClose() => void

Called once when the toast closes, whichever way (close button, timeout, swipe, Escape with focus inside the toast, manager.close). Passing this prop also implicitly renders the close button (no need to also set dismissible).

closePropsOmit<ToastCloseProps, "ref"> & RefAttributes<HTMLButtonElement>

Props for the built-in close button. className merges after the library classes; aria-label overrides labels.close; event.preventBaseUIHandler() in onClick keeps the toast open. Renders no button on its own: set dismissible or onClose to show it.

Note: Base UI 1.9 hides a toast added with priority: "high" from screen readers (aria-hidden) and announces it through a visually hidden alert instead. The toast stays hidden until keyboard focus enters the viewport, and is hidden again when focus leaves it. While it is hidden, chunks-ui keeps the toast, its Toast.Close and its Toast.Action out of the tab order (except where a tabIndex of yours wins, see Compound parts), so Tab passes over the toast: press F6, then Tab, to reach the toast and its buttons. A mouse click still focuses the toast and leaves it hidden. Other focusable elements that you put in a high-priority Toast.Root keep their tab stops (mui/base-ui#5659 ). This includes a link in the title or description of a high-priority toast: Base UI also renders those in a visually hidden copy, which screen readers announce, and the link in it is an invisible tab stop. With add(), navigate from actionProps.onClick instead. In a custom Toast.Root, render the link through the render prop of Toast.Action, with nativeButton={false} on Toast.Action and role={undefined} on the link, and do not set tabIndex on the link or in actionProps there. Base UI gives the action role="button". A role on the link wins over it. Without role={undefined}, screen readers announce the link as a button. In a render function, put role={undefined} after {...props}: props already holds role: "button", and nothing merges after your element. Biome’s useValidAriaRole flags role={undefined}. Suppress it there: its fix removes the attribute, and the link is a button again.

Base UI also hides the close button of every toast from screen readers while the toasts are collapsed, with no pointer over them and no keyboard focus in them. chunks-ui keeps the button out of the tab order for that time. Keyboard focus on a toast expands the toasts and brings the button back: from the toast, Tab reaches its action first when it has one, then its close button.

Toast.Viewport

Toast.Viewport is a thin style wrapper. It renders the toast list itself and forwards every other prop to Base UI’s Toast.Viewport; children you pass are not rendered. All per-toast behaviour (icon, iconClassName, className, dismissible, onClose, closeProps) lives on the individual add() call, not the container, so a single viewport can host a mix of dismissible and non-dismissible toasts, styled and unstyled ones, etc.

Only string-level overrides are worth knowing about here:

  • Pass className="toast-stack-viewport" to opt into the stacking pattern.
  • Otherwise, you almost never need to touch the viewport beyond mounting it.

Toast.Viewport also takes labels:

NameTypeDefault
labelsPartial<ToastLabels>

Replaces the built-in English strings, e.g. to translate them. Keys you leave out keep the default from DEFAULT_TOAST_LABELS. aria-label takes precedence over labels.viewport, and a toast’s closeProps["aria-label"] over labels.close.

Compound parts

All other compound parts (Toast.Provider, Toast.Root, Toast.Title, Toast.Description, Toast.Close, Toast.Action) are thin style wrappers and forward their props to the corresponding Base UI primitive, except that Toast.Root, Toast.Close and Toast.Action set tabIndex to -1 while Base UI hides them from screen readers, replacing a tabIndex you pass (see the note under Props). A tabIndex on the element you pass to render still wins. On a Toast.Action with a render, so does a tabIndex in actionProps. See the Base UI Toast reference  for the full prop surface.

Labels

labels replaces the built-in English text, for example to translate it. Keys you leave out keep their default from DEFAULT_TOAST_LABELS. labels.viewport names the region that Base UI announces the toasts in, and labels.close names the close button of a dismissible toast. aria-label on Toast.Viewport takes precedence over labels.viewport. See Labels.

NameTypeDefault
viewportstring

Name of the Toast.Viewport region.

"Notifications"
closestring

Name of the close button of a dismissible toast.

"Close"
Last updated on