MiHCM
feedback/@mihcm/ui·v0.21.0·stable

Toast

Brief non-blocking notification that auto-dismisses. Imperative toast() API with semantic variants, appearance styles, and configurable duration.

Toast

Brief, non-blocking notification that auto-dismisses. Built on top of sonner (website) and themed end-to-end with MiHCM tokens.

When to use

  • Action confirmation — saved, deleted, archived, sent.
  • Background process updates — upload complete, sync finished.
  • Promise lifecycle — a single row that shows loading, then success or error.
  • Non-critical errors — network hiccup with optional retry action.

When NOT to use

  • For persistent messages that require user acknowledgement — use an Alert.
  • For blocking confirmation before a destructive action — use an AlertDialog.
  • For inline field validation — use Input with invalid.

Setup

Import once and mount the Toaster near the root of your app:

// app/layout.tsx
import { Toaster } from '@mihcm/ui/Toast';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster richColors closeButton position="bottom-right" />
      </body>
    </html>
  );
}

Then call from anywhere:

import { toast } from '@mihcm/ui/Toast';
 
toast('Saved');
toast.success('Profile updated');
toast.error('Network error', { description: 'Retrying in 5s…' });

Type variants

MethodUse when
toast(msg)Neutral informational notification (no leading icon).
toast.success(msg)A completed action or positive outcome.
toast.error(msg)An error that doesn't require immediate user action.
toast.warning(msg)A non-blocking caution.
toast.info(msg)Tip or contextual hint.
toast.message(msg)Same surface as toast() — useful for explicit semantics.
toast.loading(msg)Spinner while a task is in flight. Return id to update later.
toast.promise(p, …)Auto-swap one row through loading → success / error.
toast.custom(jsx)Full design freedom. Render any JSX as the row.

Promise lifecycle

toast.promise(saveDraft(), {
  loading: 'Saving…',
  success: (data) => `Saved "${data.title}"`,
  error: (err) => `Save failed: ${err.message}`,
});

Manual update — loading then success/error

const id = toast.loading('Compiling…');
try {
  const result = await build();
  toast.success('Build succeeded', { id });
} catch (err) {
  toast.error('Build failed', { id, description: (err as Error).message });
}

Reusing the same id swaps the existing row in place — no flicker, no stack.

Action + cancel buttons

toast('Delete this draft?', {
  description: "This can't be undone.",
  action: { label: 'Delete', onClick: () => doDelete() },
  cancel: { label: 'Cancel', onClick: () => {} },
});

Custom JSX

toast.custom((id) => (
  <div className="flex items-start gap-3 rounded-lg border border-border bg-card p-4 shadow-lg">
    <Avatar src={user.avatar} />
    <div className="flex-1">
      <div className="font-semibold">{user.name} mentioned you</div>
      <p className="text-xs text-muted-foreground">{thread.preview}</p>
    </div>
    <button onClick={() => toast.dismiss(id)}>Dismiss</button>
  </div>
));

Dismissing

toast.dismiss();      // dismiss all visible toasts
toast.dismiss(id);    // dismiss a specific row by id

Composition tips

  • Mount one <Toaster /> at the app root, not inside individual pages — sonner uses a portal, so multiple mounts create duplicate stacks.
  • Use toast.promise() instead of three sequential toast.loading() / toast.success() calls — the single API does the swap atomically.
  • Reuse id (either explicit id you pass, or the one toast.loading() returns) to update an in-flight toast instead of stacking a new row.
  • Keep titles to one short sentence and use description for the detail. Long bodies wrap and look heavy.
  • For destructive prompts that block work, use AlertDialog; toasts are non-blocking by design.