Components
Toast
Transient feedback that does not steal focus. Mount <Toaster /> once near the root of the app, then fire messages from anywhere with toast() — five status variants, a promise helper, and an imperative dismiss.
Basic usage
The composite does not import next-themes — entry @comitor/ui has to run in apps that do not have it. In a Next app that does, pass resolvedTheme down, exactly as this page does; skip it and toasts follow the OS colour scheme instead of the theme the reader chose.
Variants
Five variants, passed as an option rather than as a method name. Each status icon is tinted with an -ink token, not the matching fill token: an icon is a non-text element that needs 3:1 against --popover, and text-warning measures 1.59:1 on the light palette against 6.51:1 for --warning-ink.
Description, action & duration
A toast carrying an action needs time to be reached — give it Number.POSITIVE_INFINITY and let the reader close it, rather than lengthening the default four seconds and hoping.
Following a promise
toastPromise() replaces one toast through loading → success → error. Its loading string defaults to the Vietnamese 'Đang xử lý…', so an English surface must pass all three strings; success and error also accept a function of the resolved value.
Dismissing
toast() returns the id it created. Keep it to close that one toast later — when the operation it reported finishes, or when the view it belonged to unmounts. Called with no argument, dismissToast() clears the whole stack.
Two toasters, two names
The package ships two toast surfaces. They are not aliases of each other, and only one may be mounted: sonner keeps a single global queue, so whichever one is on screen renders every toast() call in the app.
Toaster
@comitor/ui
- The everyday one. Status icons tinted with
-inktokens, and action / cancel buttons matched to the package's own Button. - Five variants under the design system's vocabulary —
destructive, noterror. - No next-themes dependency, so it works in any React app. The price is one
themeprop. - Comes with
toast(),toastPromise(),dismissToast().
SonnerToaster
@comitor/ui/shell
- A thin wrapper: it calls
useTheme()itself and points sonner's--normal-bg/--normal-textat the popover tokens and--normal-borderat--border. - No status icons — you get sonner's built-in set, untinted by the contract.
- next-themes is a hard dependency, as it is for everything at
/shell. - Exported only as
SonnerToaster/SonnerToasterProps.
Reach for Toaster from @comitor/ui by default. It is the one that speaks the status vocabulary and solves icon contrast, and threading theme through is a single prop. Take SonnerToaster only when the app is already built on the shell's ThemeProvider, you would rather not pass that prop, and plain sonner toasts are enough. Either way the package's toast() drives it — the same queue, only without the tinted icons.
import { Button, toast } from '@comitor/ui'
import { SonnerToaster } from '@comitor/ui/shell'
export function Example() {
return (
<>
<Button onClick={() => toast('Shipment released', { variant: 'success' })}>
Show toast
</Button>
{/* Reads next-themes itself — no theme prop to thread through */}
<SonnerToaster />
</>
)
}The name is deliberate, not clumsy. Toaster and ToasterProps are already taken at entry @comitor/ui by the composite, so the shell file withholds those bare names: exported, they would be dead names — /shell only re-exports the Sonner* pair — and a later export * would either break the main entry's build or quietly open a second Toaster door. Two toasters with two names only holds while the bare name has no way out.
ToasterProps
ToasterProps is sonner's own props type, re-exported from @comitor/ui. The defaults below are the composite's where it sets one, and sonner's otherwise.
| Prop | Type | Default | Description |
|---|---|---|---|
theme | 'light' | 'dark' | 'system' | 'system' | Colour scheme of the toast surface. The composite deliberately does not read next-themes — pass resolvedTheme from useTheme() yourself, or toasts follow the OS prefers-color-scheme and drift from the theme the user picked. |
position | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' | 'bottom-right' | Corner the stack grows from. Set by the composite, not by sonner. |
duration | number | 4000 | Default lifetime in milliseconds for every toast; a per-toast duration overrides it. |
visibleToasts | number | 3 | How many toasts stay on screen; older ones collapse behind the stack. |
expand | boolean | false | Keep the stack expanded instead of collapsing it until hover. |
closeButton | boolean | false | Render a close control on every toast. Worth turning on wherever toasts carry an action. |
richColors | boolean | false | Turns on sonner’s own saturated per-status palette — hard-coded hsl() values that belong to no Comitor token and do not follow data-contrast="high". Leave it off; the composite already carries status colour in the icon. |
gap | number | 14 | Vertical gap between stacked toasts, in pixels. |
offset | string | number | { top?, right?, bottom?, left? } | '24px' | Distance from the viewport edges on desktop. |
mobileOffset | string | number | { top?, right?, bottom?, left? } | '16px' | Same, below the mobile breakpoint. |
hotkey | string[] | ['altKey', 'KeyT'] | Key combination that moves focus into the toast region; it is also read out in the region’s aria-label. |
dir | 'ltr' | 'rtl' | 'auto' | — | Writing direction. Left unset — or set to auto — sonner reads the resolved direction off the document element; pass ltr or rtl to force one. |
icons | { success?, warning?, error?, info?, loading?, close? } | — | The composite already supplies the four status icons tinted with -ink tokens. Passing this replaces the whole object, tints included. |
toastOptions | ToastOptions (sonner) | — | Class names for the toast surface, title, description, action and cancel buttons are set by the composite. Passing this replaces them wholesale — prefer className on the toast you are firing. |
className | string | 'toaster group' | Class on the toast list. The group hook is what the composite’s class names key off, so extend rather than replace it. |
...rest | ToasterProps | — | Every remaining sonner Toaster prop is forwarded — and, because rest is spread last, overrides the composite’s own icons and toastOptions. |
ToastOptions
The second argument to toast(). This is the package's own narrowed type — not sonner's ExternalToast — so anything outside these five keys is deliberately unavailable.
| Prop | Type | Default | Description |
|---|---|---|---|
description | ReactNode | — | Second line under the title, in muted-foreground. |
variant | 'default' | 'success' | 'warning' | 'destructive' | 'info' | 'default' | Picks the status icon. These are the design system’s names — destructive, not error. |
duration | number | 4000 | Lifetime in milliseconds. Number.POSITIVE_INFINITY keeps the toast until the user closes it. |
action | { label: string; onClick: () => void } | — | One action button, styled to match the package’s primary Button. |
id | string | number | — | Reuse an id to update a toast in place (the “saving…” → “saved” flow), or to dismiss exactly that one later. |
Functions
All three are client-only: they are exported from a 'use client' module, so calling one from a Server Component throws rather than failing quietly. Fire toasts from event handlers and effects.
toast(title, options?)
| Prop | Type | Default | Description |
|---|---|---|---|
titlerequired | ReactNode | — | The message. Keep the meaning in this string — the status icon is decoration. |
options | ToastOptions | {} | Everything in the table above. |
returns | string | number | — | The toast id, for a later update or dismissToast(id). |
toastPromise(promise, messages)
| Prop | Type | Default | Description |
|---|---|---|---|
promiserequired | Promise<T> | — | The task to follow. The toast switches state when it settles. |
messages.loading | ReactNode | 'Đang xử lý…' | Shown while the promise is pending. The package default is Vietnamese — pass an English string in an English UI. |
messages.successrequired | ReactNode | ((data: T) => ReactNode) | — | Shown on resolve; as a function it receives the resolved value. |
messages.errorrequired | ReactNode | ((error: unknown) => ReactNode) | — | Shown on reject; as a function it receives the thrown value. |
returns | string | number | { unwrap() } | — | sonner’s promise handle; await handle.unwrap() to get the original value back (or re-throw the original error). |
dismissToast(id?)
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | number | — | The toast to close. Omit it to dismiss every toast currently on screen. |
returns | void | — | Dismissing an id that is gone is a no-op, so it is safe to call unconditionally. |
Accessibility
- The toast region is a
<section aria-live="polite">, so new messages are announced without focus moving — a toast never interrupts what someone is typing. - Alt + T moves focus into the region (the
hotkeyprop), each toast is focusable, and Tab reaches its action and close controls. The combination is read out in the region's label. - Auto-dismiss pauses while the pointer is over the stack or focus is inside it. A toast that carries an action should still set
duration: Number.POSITIVE_INFINITY— a four-second window to find a button fails WCAG 2.2.1. - The status icon is decoration and carries no accessible name. Keep the meaning in the title: “Could not reach the server”, never a bare “Failed.” tinted red.
- Icons use
-inktokens for the 3:1 non-text threshold on--popover, and the cancel button carriesborder-control-edgebecause itsbg-mutedfill sits only 1.07:1 off the surface — without the border it reads as plain text (WCAG 1.4.11).