Instruction file imported from SunnyChopper/personal-os-web (
.cursor/rules/toast-notifications.mdc). Copyright stays with the author.
Toast Notifications
Standards for displaying transient feedback messages.
Toast Types
type ToastType = 'success' | 'error' | 'warning' | 'info';
interface Toast {
id: string;
type: ToastType;
title: string;
message?: string;
duration?: number;
}
Toast Component
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react';
const icons = {
success: CheckCircle,
error: XCircle,
warning: AlertTriangle,
info: Info,
};
const styles = {
success: 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200',
error: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200',
info: 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-200',
};
function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) {
const Icon = icons[toast.type];
useEffect(() => {
const timer = setTimeout(onDismiss, toast.duration || 5000);
return () => clearTimeout(timer);
}, [toast.duration, onDismiss]);
return (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
className={cn(
'flex items-start gap-3 p-4 rounded-lg border shadow-lg max-w-sm',
styles[toast.type]
)}
>
<Icon className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="font-medium">{toast.title}</p>
{toast.message && (
<p className="text-sm opacity-90 mt-1">{toast.message}</p>
)}
</div>
<button
onClick={onDismiss}
className="p-1 hover:bg-black/10 rounded transition-colors"
>
<X className="w-4 h-4" />
</button>
</motion.div>
);
}
Toast Container
function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {
return (
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
<AnimatePresence>
{toasts.map(toast => (
<ToastItem
key={toast.id}
toast={toast}
onDismiss={() => onDismiss(toast.id)}
/>
))}
</AnimatePresence>
</div>
);
}
Toast Context
interface ToastContextType {
toasts: Toast[];
showToast: (toast: Omit<Toast, 'id'>) => void;
dismissToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((toast: Omit<Toast, 'id'>) => {
const id = crypto.randomUUID();
setToasts(prev => [...prev, { ...toast, id }]);
}, []);
const dismissToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, showToast, dismissToast }}>
{children}
<ToastContainer toasts={toasts} onDismiss={dismissToast} />
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within ToastProvider');
return context;
}
Usage Patterns
function TaskActions() {
const { showToast } = useToast();
const handleDelete = async () => {
try {
await tasksService.delete(taskId);
showToast({
type: 'success',
title: 'Task deleted',
message: 'The task has been removed.',
});
} catch (error) {
showToast({
type: 'error',
title: 'Failed to delete',
message: 'Please try again.',
});
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Helper Functions
// Convenience methods
const toast = {
success: (title: string, message?: string) =>
showToast({ type: 'success', title, message }),
error: (title: string, message?: string) =>
showToast({ type: 'error', title, message }),
warning: (title: string, message?: string) =>
showToast({ type: 'warning', title, message }),
info: (title: string, message?: string) =>
showToast({ type: 'info', title, message }),
};
// Usage
toast.success('Saved!');
toast.error('Something went wrong', 'Please try again later.');
Best Practices
- Keep messages concise (title: 2-4 words, message: 1 sentence)
- Auto-dismiss after 3-5 seconds
- Allow manual dismissal
- Stack multiple toasts vertically
- Position in corner (top-right recommended)
- Don't interrupt user flow
- Use appropriate type for context