Sonner — spec¶
Synthesized from shadcn-ui
sonner(the Sonner library, by emilkowalski). The modern toast-of-choice in shadcn-based projects. Stacks beautifully, supports rich actions, promise wrappers.
Sonner vs Toast vs Snackbar¶
Sonner is shadcn's recommended toast library, distinct from older toast / snackbar patterns:
- Stacking: cards stack with depth, expand on hover.
- Promise wrapper: toast.promise(promise, { loading, success, error }).
- Rich content: titles, descriptions, action + cancel buttons.
- Sound (optional): subtle confirmation chime.
For shadcn-based projects: use Sonner instead of writing custom Toast.
Anatomy¶
┌────────────────────────────┐
│ ✓ Saved successfully │
│ Your changes are live. │
│ [Undo] [Dismiss] │
└────────────────────────────┘
┌────────────────────────────┐
│ Earlier toast (background) │
└────────────────────────────┘
↑ stack expands on hover
API¶
import { Toaster, toast } from "sonner";
// Once at app root:
<Toaster position="bottom-right" richColors />
// Anywhere:
toast.success("저장됐어요", {
description: "변경 사항이 적용됐어요.",
action: { label: "실행 취소", onClick: undo },
});
toast.error("저장 실패");
toast.info("새 알림");
toast.warning("연결 불안정");
toast.loading("처리 중...");
toast.promise(savePromise, {
loading: "저장 중...",
success: "저장됐어요",
error: "저장 실패",
});
// Dismiss
const id = toast("Custom");
toast.dismiss(id);
toast.dismiss(); // all
Toaster props¶
| Prop | Type | Default | Description |
|---|---|---|---|
position |
"top-left" \| "top-center" \| "top-right" \| "bottom-left" \| "bottom-center" \| "bottom-right" |
"bottom-right" |
Anchor |
richColors |
boolean |
false |
Use semantic colors (green success, red error) |
closeButton |
boolean |
false |
Show × on each toast |
expand |
boolean |
false |
Expand stack by default (instead of on hover) |
duration |
number |
4000 |
Ms |
visibleToasts |
number |
3 |
Max visible at once |
theme |
"light" \| "dark" \| "system" |
"system" |
Theme |
toastOptions |
object |
— | Default options for all toasts |
Per-toast options¶
| Field | Description |
|---|---|
description |
Subtitle text |
action |
{ label, onClick } — primary action |
cancel |
{ label, onClick } — secondary cancel |
duration |
Override default |
id |
For deduping / programmatic dismiss |
important |
Don't auto-dismiss |
icon |
Custom icon |
Promise wrapper¶
toast.promise(api.save(data), {
loading: "저장 중...",
success: (result) => `${result.title} 저장됨`,
error: (err) => `저장 실패: ${err.message}`,
});
Single API replaces the manual try { setLoading(true); ... } catch { ... } flow. Sonner handles state transitions.
States¶
| State | Visual |
|---|---|
| Pending (promise loading) | Spinner + loading text |
| Success | Green check + title + description |
| Error | Red X + title + description |
| Persistent (important) | No auto-dismiss |
| Stacked (idle) | Compressed visually |
| Stacked (hovered) | Expand to show all |
Tokens consumed¶
--toast-bg (per severity)
--toast-fg
--toast-success / -error / -info / -warning (richColors)
--toast-action-fg (button)
--toast-shadow
--radius-md
--motion-medium (slide / expand)
--ease-out
--z-toast
Accessibility¶
- Each toast:
role="status"(success/info) orrole="alert"(error). aria-livefollows role.- Keyboard: Tab can focus toast actions; Esc dismisses.
- Touch: swipe left/right to dismiss (mobile gesture).
Code example — Korean fintech save flow¶
async function handleSave() {
toast.promise(
api.saveTransaction(data),
{
loading: "결제 처리 중...",
success: (result) => ({
title: "결제가 완료되었어요",
description: `${formatKRW(result.amount)} 결제 완료`,
action: {
label: "영수증 보기",
onClick: () => navigate(`/receipts/${result.id}`),
},
}),
error: (err) => `결제 실패: ${err.message}`,
}
);
}
Don't¶
- Don't use Sonner for in-app errors that block user. Use AlertDialog.
- Don't queue 5+ toasts. Cap visible at 3.
- Don't autoHide < 3s for messages with actions. Users need time to act.
- Don't mix Sonner with another Toast library — pick one.
References¶
- shadcn-ui:
sonner - Sonner library by emilkowalski