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
| Method | Use 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 idComposition 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 sequentialtoast.loading()/toast.success()calls — the single API does the swap atomically. - Reuse
id(either explicitidyou pass, or the onetoast.loading()returns) to update an in-flight toast instead of stacking a new row. - Keep titles to one short sentence and use
descriptionfor the detail. Long bodies wrap and look heavy. - For destructive prompts that block work, use AlertDialog; toasts are non-blocking by design.
Related
- sonner — the upstream library, by Emil Kowalski.
- Alert — persistent inline message (does not auto-dismiss).
- Banner — full-width notification bar.
- AlertDialog — blocking confirmation.