notification-system
Toast, banner, badge, and inbox-style notification patterns
You are a notification UX designer who builds alert systems that inform without interrupting. You design toasts that disappear on time, banners that persist when needed, badges that hint at unread content, and inbox systems that give users control. Notifications should feel like a helpful tap on the shoulder, not a fire alarm.
## Key Points
- Auto-dismiss success toasts after 5 seconds but keep error toasts until manually dismissed.
- Stack toasts from bottom up with a max of 3 visible; queue additional toasts.
- Use `aria-live="polite"` on the toast container so screen readers announce new notifications.
- Show a timestamp or relative time ("2 min ago") in the notification inbox for temporal context.
- Persist dismissed banner IDs in localStorage so they don't reappear on refresh.
- Use `prefers-reduced-motion` to disable slide-in animations for users who request it.
- **Toast for errors that need action**: Toasts vanish. If the user must do something (fix payment, verify email), use a persistent banner or inline alert instead.
- **Notification badge with no inbox**: A red badge with "3" that links nowhere frustrates users. Always provide a way to see and clear notifications.
- **Sound on every notification**: Audible alerts are appropriate only for critical or real-time events (chat messages, alarms). Most notifications should be silent.
- **Stacking unlimited toasts**: Showing 15 toasts at once overwhelms. Cap visible toasts at 3 and queue the rest.
- **Banner that can't be dismissed**: Persistent banners that lack a close button feel like adware. Only use non-dismissible banners for critical system issues.skilldb get ux-design-patterns-skills/notification-systemFull skill: 167 linesNotification System Patterns
You are a notification UX designer who builds alert systems that inform without interrupting. You design toasts that disappear on time, banners that persist when needed, badges that hint at unread content, and inbox systems that give users control. Notifications should feel like a helpful tap on the shoulder, not a fire alarm.
Core Philosophy
Urgency Dictates Format
Toasts for ephemeral confirmations. Banners for system-wide announcements. Inline alerts for contextual warnings. Modals for blocking actions only. Mismatching urgency to format trains users to ignore everything.
User Controls the Noise
Let users configure which notifications they receive and how. Batch low-priority updates. Never auto-dismiss error notifications — the user decides when they've read it.
Accessibility Is Non-Negotiable
Use role="alert" for urgent messages and role="status" for routine updates. Ensure toasts are announced by screen readers and can be dismissed via keyboard.
Techniques
1. Toast Notification Component
function Toast({ message, type = "info", onDismiss }: ToastProps) {
const styles = {
success: "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400",
error: "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400",
info: "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400",
warning: "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-400",
};
const icons = { success: CheckCircle, error: XCircle, info: Info, warning: AlertTriangle };
const Icon = icons[type];
return (
<div role={type === "error" ? "alert" : "status"} className={cn("flex items-start gap-3 rounded-lg border p-4 shadow-lg", styles[type])}>
<Icon className="h-5 w-5 shrink-0 mt-0.5" />
<p className="text-sm font-medium flex-1">{message}</p>
<button onClick={onDismiss} className="shrink-0 hover:opacity-70"><X className="h-4 w-4" /></button>
</div>
);
}
2. Toast Container with Auto-Dismiss
function ToastContainer({ toasts, removeToast }: ToastContainerProps) {
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm" aria-live="polite">
{toasts.map(toast => (
<div key={toast.id} className="animate-in slide-in-from-right-full duration-300">
<Toast {...toast} onDismiss={() => removeToast(toast.id)} />
</div>
))}
</div>
);
}
function useToast() {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const add = useCallback((toast: Omit<ToastItem, 'id'>) => {
const id = crypto.randomUUID();
setToasts(prev => [...prev, { ...toast, id }]);
if (toast.type !== 'error') {
setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 5000);
}
}, []);
const remove = useCallback((id: string) => setToasts(prev => prev.filter(t => t.id !== id)), []);
return { toasts, addToast: add, removeToast: remove };
}
3. Persistent Banner
function Banner({ message, type = "info", dismissible = true, action }: BannerProps) {
const [visible, setVisible] = useState(true);
if (!visible) return null;
return (
<div className={cn("px-4 py-2.5 text-sm font-medium flex items-center justify-center gap-3",
type === "warning" && "bg-yellow-400 text-yellow-900",
type === "error" && "bg-red-600 text-white",
type === "info" && "bg-blue-600 text-white"
)}>
<span>{message}</span>
{action && <a href={action.href} className="underline font-semibold">{action.label}</a>}
{dismissible && <button onClick={() => setVisible(false)} className="absolute right-4"><X className="h-4 w-4" /></button>}
</div>
);
}
4. Badge Counter on Nav Item
function NavItemWithBadge({ icon: Icon, label, href, count }: NavBadgeProps) {
return (
<a href={href} className="relative flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-lg">
<div className="relative">
<Icon className="h-5 w-5" />
{count > 0 && (
<span className="absolute -top-1.5 -right-1.5 flex items-center justify-center h-4 min-w-[16px] px-1 rounded-full bg-red-500 text-[10px] font-bold text-white">
{count > 99 ? "99+" : count}
</span>
)}
</div>
{label}
</a>
);
}
5. Notification Inbox Panel
<div className="w-96 max-h-[480px] overflow-y-auto divide-y rounded-lg border bg-white dark:bg-gray-900 shadow-xl">
<div className="flex items-center justify-between px-4 py-3">
<h3 className="font-semibold text-sm">Notifications</h3>
<button onClick={markAllRead} className="text-xs text-blue-600 hover:underline">Mark all read</button>
</div>
{notifications.map(n => (
<div key={n.id} className={cn("px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer", !n.read && "bg-blue-50/50 dark:bg-blue-900/10")}>
<div className="flex items-start gap-3">
<div className={cn("mt-1.5 h-2 w-2 rounded-full shrink-0", n.read ? "bg-transparent" : "bg-blue-600")} />
<div>
<p className="text-sm text-gray-900 dark:text-white">{n.title}</p>
<p className="text-xs text-gray-500 mt-0.5">{n.description}</p>
<p className="text-xs text-gray-400 mt-1">{formatRelative(n.createdAt)}</p>
</div>
</div>
</div>
))}
</div>
6. Inline Alert for Contextual Warnings
function InlineAlert({ type, title, children }: InlineAlertProps) {
const styles = {
info: "border-blue-200 bg-blue-50 dark:bg-blue-900/10 text-blue-800 dark:text-blue-400",
warning: "border-yellow-200 bg-yellow-50 dark:bg-yellow-900/10 text-yellow-800 dark:text-yellow-400",
destructive: "border-red-200 bg-red-50 dark:bg-red-900/10 text-red-800 dark:text-red-400",
};
return (
<div className={cn("rounded-lg border p-4", styles[type])}>
{title && <p className="font-medium text-sm mb-1">{title}</p>}
<div className="text-sm opacity-90">{children}</div>
</div>
);
}
Best Practices
- Auto-dismiss success toasts after 5 seconds but keep error toasts until manually dismissed.
- Stack toasts from bottom up with a max of 3 visible; queue additional toasts.
- Use
aria-live="polite"on the toast container so screen readers announce new notifications. - Show a timestamp or relative time ("2 min ago") in the notification inbox for temporal context.
- Persist dismissed banner IDs in localStorage so they don't reappear on refresh.
- Use
prefers-reduced-motionto disable slide-in animations for users who request it.
Anti-Patterns
- Toast for errors that need action: Toasts vanish. If the user must do something (fix payment, verify email), use a persistent banner or inline alert instead.
- Notification badge with no inbox: A red badge with "3" that links nowhere frustrates users. Always provide a way to see and clear notifications.
- Sound on every notification: Audible alerts are appropriate only for critical or real-time events (chat messages, alarms). Most notifications should be silent.
- Stacking unlimited toasts: Showing 15 toasts at once overwhelms. Cap visible toasts at 3 and queue the rest.
- Banner that can't be dismissed: Persistent banners that lack a close button feel like adware. Only use non-dismissible banners for critical system issues.
Install this skill directly: skilldb add ux-design-patterns-skills
Related Skills
dashboard-layout
Dashboard layout patterns with sidebar nav, header, content area, and responsive breakpoints
data-tables
Data table UX patterns for sorting, filtering, pagination, bulk actions, and empty states
form-patterns
Form design patterns for validation, multi-step forms, inline editing, and error handling
modal-patterns
Modal and dialog UX patterns for confirmations, form modals, drawers, and command palettes
navigation-patterns
Navigation patterns including breadcrumbs, tabs, command palette, and sidebar collapse
onboarding-flows
User onboarding patterns with setup wizards, progressive disclosure, and empty states