@akhatmirzo/ui

Toast

Short notifications with an imperative toast() API, promise tracking, an action button and stacking.

Import

import { toast, Toaster } from '@akhatmirzo/ui';

When to use

  • To report the result of an action — "Saved", "Link copied", "Payment received". It doesn't interrupt the user and goes away on its own.
  • Undo — after a delete, offer a "Restore" button through action. This is often friendlier than confirm(), because you don't stop the user with a question.
  • Progress of a longer task — with toast.promise() or toast.loading() + { id }, one toast moves from "loading" to the result.
  • A question that needs an answer calls for confirm(), not a toast. A field validation error belongs next to the field — in the error prop of Field.

Toaster setup

toast() writes to a module-level store; <Toaster /> renders it. Your app needs exactly one <Toaster /> — without it, toast() doesn't throw, but nothing shows up.

app/providers.tsx
'use client';

import { Toaster } from '@akhatmirzo/ui';
import type { ReactNode } from 'react';

export function Providers({ children }: { children: ReactNode }) {
  return (
    <>
      {children}
      <Toaster position="bottom-right" />
    </>
  );
}

After that, call toast() from anywhere — a component, an event handler, or even a plain function outside React (say, the error handler of your API client).

Types

FunctionIconDefault duration
toast()none4 s
toast.success()green ✓4 s
toast.error()red !6 s
toast.warning()yellow triangle4 s
toast.info()blue i4 s
toast.loading()spinneruntil updated

Each function returns the toast id. toast.dismiss(id) closes one toast; toast.dismiss() closes them all.

Description and action

description adds a second line. action adds a small button on the right: { label, onClick }. Clicking it closes the toast; to keep it open, call event.preventDefault() inside onClick.

toast('Product deleted', {
  description: 'iPhone 15 Pro, 256 GB',
  action: { label: 'Restore', onClick: () => restoreProduct(id) },
});

toast.promise()

Tracks a promise: loading first, then success or error — all in a single toast. Pass functions for success and error to receive the result or the error. You can also pass a function that returns the promise. toast.promise() returns the original promise, so you can await it or attach .catch().

const order = await toast.promise(api.saveOrder(data), {
  loading: 'Saving...',
  success: (o) => `Order #${o.id} saved`,
  error: (e) => `Error: ${(e as Error).message}`,
});

Updating by id

With { id }, the toast with that id is updated (or created with that id if it doesn't exist). You can change its type, title and description; the timer starts over.

Stacking

At most 3 toasts are visible at a time (visibleToasts); the rest wait in line with their timers paused. Visible toasts sit in a compact stack that fans out on hover or focus, and all timers pause meanwhile. Swipe a toast sideways to dismiss it.

Props

toast(title, options?)

title is a ReactNode. Every variant (toast.success ...) takes the same options except type.

PropTypeDefaultDescription
idstring | number—Updates the toast with this id, or creates one with it
type'default' | 'success' | 'error' | 'warning' | 'info' | 'loading''default'toast() only; the variants set it from their name
descriptionReactNode—Second line
action{ label: ReactNode; onClick?: (event) => void; altText?: string }—Button on the right. altText — for screen readers when label isn't text
durationnumber4000ms. error — 6000, loading — Infinity
iconReactNode—Replaces the status icon; null hides it
onDismiss(id: ToastId) => void—Called whenever the toast closes, for any reason

toast.promise(promise, messages, options?)

PropTypeDefaultDescription
promiserequiredPromise<T> | (() => Promise<T>)—The promise to track, or a function that returns it
messages.loadingrequiredReactNode—Pending text
messages.successrequiredReactNode | ((data: T) => ReactNode)—Success text
messages.errorrequiredReactNode | ((error: unknown) => ReactNode)—Error text
optionsOmit<ToastOptions, 'type' | 'duration'>—description, action, icon ... (applied at every stage)

Returns the original Promise<T>.

Toaster

PropTypeDefaultDescription
position'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right''bottom-right'On phones (< 640px), always at the bottom and full width
durationnumber4000Default duration, ms (error — 6000, loading — infinite)
visibleToastsnumber3How many toasts are visible at once
gapnumber8Gap between toasts when the stack is expanded, px
closeButtonbooleanfalseA ✕ button on every toast
hotkeystring[]['F8']Key that moves focus to the toast region
labels{ region?: string; toast?: string; close?: string }—Screen reader texts (see below)
classNamestring—Class for the toast region (viewport)

Default labels are Uzbek: region — 'Bildirishnomalar ({hotkey})' ({hotkey} is replaced with the key name), toast — 'Bildirishnoma' (the word announced before each toast), close — 'Yopish'. For an English app:

<Toaster labels={{ region: 'Notifications ({hotkey})', toast: 'Notification', close: 'Close' }} />

Data attributes

AttributeOnValue
data-typetoastdefault · success · error · warning · info · loading
data-indextoastposition in the stack (0 is the front)
data-fronttoastpresent on the front toast
data-expandedtoast, regionpresent while the stack is expanded
data-positionregionthe position value

Keyboard

KeyAction
F8Moves focus to the toast region (hotkey)
TabMoves between toasts and their buttons
EscCloses the focused toast
Enter / SpacePresses the action or ✕ button

Accessibility

  • Announcements. Toasts render with role="status", so screen readers announce them without interrupting. The error type uses role="alert" and is announced right away.
  • Timers pause while the pointer is over the toasts or focus is inside the region, so users have time to read.
  • Reachable by keyboard. F8 (or your hotkey) moves focus to the toast region, which is labeled "Bildirishnomalar (F8)" by default.
  • Action buttons. If label isn't text (an icon, say), provide altText for screen readers.
  • Queued toasts are inert, so focus never lands on them.
  • Reduced motion. With prefers-reduced-motion, the slide becomes a fade.

On this page