Toast

A succinct message displayed temporarily to provide feedback about an action. Supports multiple variants, promise tracking, custom rendering, and global usage outside React.

View Source

#Installation

npx @dinachi/cli@latest add toast

#Usage

tsx
import {
  Toast,
  ToastProvider,
  ToastPortal,
  ToastViewport,
  ToastRoot,
  ToastContent,
  ToastTitle,
  ToastDescription,
  ToastAction,
  ToastClose,
  ToastPositioner,
  ToastArrow,
  ToastList,
  useToastManager,
  createToastManager,
} from '@/components/ui/toast'

#Examples

#Toast Manager

The useToastManager hook (inside React) and createToastManager factory (outside React) return the same manager object.

#Methods

#add(options)

Create a toast. Returns the toast ID.

tsx
const id = manager.add({ title: 'Saved', type: 'success' });

#update(id, options)

Modify an existing toast in-place — title, description, type, timeout, or action. This is what powers progress toasts, multi-step flows, and retry patterns.

tsx
manager.update(id, {
  title: 'Upload complete',
  type: 'success',
  timeout: 5000,
});

#close(id?)

Dismiss a specific toast by ID, or dismiss all toasts when called with no arguments.

tsx
manager.close(id);   // close one toast
manager.close();     // close all toasts

#promise(promise, options)

Track a promise with automatic loading, success, and error states. Each state accepts full toast options or a function that receives the resolved value / error.

tsx
manager.promise(
  fetch('/api/save', { method: 'POST' }),
  {
    loading: { title: 'Saving...', description: 'Please wait.' },
    success: (data) => ({ title: 'Saved', description: 'Changes applied.' }),
    error: (err) => ({ title: 'Error', description: err.message }),
  }
);

#subscribe(listener)

Observe toast state changes from external systems. Returns an unsubscribe function.

tsx
const unsubscribe = manager.subscribe(() => {
  console.log('Toasts changed:', manager.toasts);
});

#Toast Options

All methods that create or modify a toast accept these options:

tsx
{
  title: ReactNode,
  description: ReactNode,
  type: 'success' | 'error' | 'warning' | 'loading' | string,
  timeout: number,           // ms — use 0 to disable auto-dismiss
  priority: 'low' | 'high',
  actionProps: ButtonHTMLAttributes,
  data: Record<string, unknown>,  // custom data for renderToast
  onClose: () => void,
  onRemove: () => void,
}

#Global Toast Manager

The CLI creates lib/toast.ts with a shared manager instance. Connect it to your React tree once, then use it from anywhere.

Create the shared instance:

tsx
// lib/toast.ts — created by the CLI
import { createToastManager } from '@/components/ui/toast';
export const toastManager = createToastManager();
tsx
// providers.tsx
import { Toast } from '@/components/ui/toast';
import { toastManager } from '@/lib/toast';
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <Toast toastManager={toastManager}>
      {children}
    </Toast>
  );
}
ts
// api-client.ts
import { toastManager } from '@/lib/toast';
 
export async function apiCall() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error('Request failed');
    return res.json();
  } catch (err) {
    toastManager.add({ title: err.message, type: 'error' });
    throw err;
  }
}

#Custom Rendering

Pass renderToast to ToastList or the Toast convenience wrapper to control toast layout. The default layout is used as a fallback when renderToast is not provided.

tsx
<ToastList
  renderToast={(toast) => (
    <ToastContent>
      <div className="flex items-start gap-3">
        <MyIcon type={toast.type} />
        <div>
          <ToastTitle>{toast.title}</ToastTitle>
          <ToastDescription>{toast.description}</ToastDescription>
        </div>
      </div>
      <ToastClose />
    </ToastContent>
  )}
/>

Custom data is accessible via toast.data. Pass it when creating the toast:

tsx
manager.add({
  title: 'New message',
  data: { avatarUrl: '/img/user.png', sender: 'Alice' },
});

Then read it in your render function:

tsx
// In renderToast:
<img src={toast.data?.avatarUrl} alt={toast.data?.sender} />

#The stack

#Depth, not a list

Three notifications should cost the same screen area as one until the reader shows interest. Collapsed, the toasts behind the front one are pushed back with a scale and a small vertical offset, so the queue reads as depth rather than as a list that has been cropped. Hovering or focusing it expands it into the real column.

Position is transform, so a toast entering or leaving does not have to wait for the stack to finish opening. Height is the one exception: collapsed, every toast is cut to the front one's height, or a taller card behind a one-line toast juts out from under it.

limit on the provider is what decides how many toasts the stack holds at once. Past three or so, more depth stops reading as more items.

#Timing is not its job

Nothing in the viewport counts. timeout on the provider, or on a single add(), is what decides how long a toast lasts, and timeout: 0 keeps one until it is dismissed by hand. Base UI pauses those timers while the viewport is hovered or focused, which is the same signal that expands the stack, so there is no second clock to keep in step.

tsx
<ToastProvider timeout={5000}>
 
// A warning the reader has not seen is not a warning that has been delivered.
add({ title: "Quota at 90%", timeout: 0 })

#Accessibility

  • The viewport is a live region. It carries role="region" and aria-live="polite", and toasts sent with priority: 'high' are mirrored into an assertive region so they interrupt. Announce nothing yourself; a second announcement is a second notification as far as a screen reader is concerned.
  • Each toast is a labelled dialog. ToastTitle and ToastDescription are what supply the name and the description, which is why they belong inside the toast rather than in markup around it. A toast with neither is announced as an empty dialog.
  • F6 moves focus to the viewport from anywhere on the page, and the toasts are reachable from there without a mouse. This is why an action a toast offers must be a real button inside it.
  • Hovering or focusing the viewport pauses every countdown, so a toast cannot expire while it is being read or while its action is being reached.
  • timeout: 0 for anything that must be acted on. A countdown is a guess at reading speed, and it is wrong for someone using a screen reader or a switch.
  • The stack expands on focus, not only on hover. A keyboard reader reaching the second toast's action cannot be asked to hover first. Swiping is a mouse and touch gesture only, so dismissing is otherwise the button: give every toast one.

#API Reference

PropTypeDefaultDescription
Toast.limitnumber3Maximum number of visible toasts
Toast.timeoutnumber5000Auto-dismiss timeout in milliseconds. Use 0 to prevent auto-dismiss.
Toast.toastManagerToastManagerExternal toast manager created via createToastManager() for global usage
Toast.renderToast(toast: ToastObject) => ReactNodeCustom render function for toasts. Receives the toast object and returns JSX. Falls back to the default layout when not provided.
ToastRoot.variant'default' | 'destructive' | 'success' | 'warning' | 'loading''default'The visual style variant of the toast
ToastRoot.toastToastObjectThe toast data object from the manager (required)
ToastRoot.swipeDirectionArray<'up' | 'down' | 'left' | 'right'>['down', 'right']The directions in which the toast can be swiped to dismiss
ToastList.renderToast(toast: ToastObject) => ReactNodeCustom render function for toasts. Same as Toast.renderToast but applied directly to ToastList.
ToastClose.childrenReactNodeCustom close icon. Renders an XIcon from lucide-react by default.
ToastPositioner.toastToastObjectThe toast data object (required). Positions the toast relative to an anchor element.
ToastPositioner.anchorElement | nullThe element to anchor the toast to.
ToastPositioner.side'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start''top'Which side of the anchor to position the toast on.
ToastPositioner.align'start' | 'center' | 'end''center'Alignment along the anchor edge.
ToastPositioner.sideOffsetnumber0Distance in pixels from the anchor.
ToastArrow.classNamestringAdditional classes for the decorative arrow. Styled with directional data-side attributes.
manager.add()(options: { title?, description?, type?, timeout?, actionProps?, data?, priority? }) => stringCreates a toast notification and returns its ID
manager.update()(id: string, updates) => voidUpdates an existing toast's title, description, type, timeout, or actionProps
manager.close()(id?: string) => voidCloses a toast by its ID, or dismisses all toasts when called with no arguments
manager.promise()(promise, { loading, success, error }) => PromiseBinds a promise to a toast that auto-transitions through loading, success, and error states
manager.subscribe()(listener: () => void) => () => voidObserves toast state changes. Returns an unsubscribe function.