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:
| Field | Purpose |
|---|---|
icon | Component rendered in the leading slot. Receives a className prop pre-populated with default sizing (size-5 shrink-0 self-start). |
iconClassName | Extra classes merged onto icon — use for color. |
className | Extra 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:
dissmissableis a deprecated misspelled alias ofdismissible. When both are set,dismissiblewins.
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:
| Class | Where | What it does |
|---|---|---|
toast-stack-viewport | Toast.Viewport | Overrides the default flex layout so children can absolute-stack at the same anchor point. |
toast-stack | each toast’s className | Drives 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
| Part | Description |
|---|---|
Toast.Provider | Context 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.Viewport | Fixed-position container that renders all active toasts. |
Toast.Root | Individual toast wrapper with slide/fade enter/exit transitions. |
Toast.Title | Bold toast heading. |
Toast.Description | Supporting text in text-muted-foreground. |
Toast.Close | Icon 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.Action | Optional action button rendered inside the toast. |
Toast.useToast | Hook 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.
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, itsToast.Closeand itsToast.Actionout of the tab order (except where atabIndexof 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-priorityToast.Rootkeep their tab stops (mui/base-ui#5659 ). This includes a link in thetitleordescriptionof 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. Withadd(), navigate fromactionProps.onClickinstead. In a customToast.Root, render the link through therenderprop ofToast.Action, withnativeButton={false}onToast.Actionandrole={undefined}on the link, and do not settabIndexon the link or inactionPropsthere. Base UI gives the actionrole="button". Aroleon the link wins over it. Withoutrole={undefined}, screen readers announce the link as a button. In arenderfunction, putrole={undefined}after{...props}:propsalready holdsrole: "button", and nothing merges after your element. Biome’suseValidAriaRoleflagsrole={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:
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.
| Name | Type | Default |
|---|---|---|
viewport | stringName of the | "Notifications" |
close | stringName of the close button of a dismissible toast. | "Close" |