A robust toast notification system requires careful consideration of display priorities, user interaction patterns, and accessibility compliance while maintaining optimal performance and developer experience through clean component architecture and flexible configuration options.
1. Problem Statement & Requirements
Clarifying questions
- What target browsers/devices are we supporting? (e.g., mobile-first, desktop-only, IE11 support)
- Do we need to support real-time notifications from server push or are they triggered by user actions?
- Should toast notifications be dismissible by user interaction or auto-dismiss after a timeout?
- Do we need to support offline scenarios where notifications might queue until connection is restored?
- What accessibility requirements are there? (e.g., screen reader support, keyboard navigation)
- Do we need to support internationalization for toast messages?
- Are there existing design system components we should integrate with?
Functional requirements
- Display toast notifications with different severity levels (success, error, warning, info)
- Support custom message content and optional icon
- Auto-dismiss after configurable timeout (default 5 seconds)
- Manual dismiss capability via close button or keyboard
- Queue management for multiple concurrent toasts
- Support for rich content (HTML, links, markdown)
- Ability to pause toast queue when user is interacting with page
- Configurable position (top-right, top-left, bottom-right, bottom-left)
- Support for stacking multiple toasts simultaneously
Non-functional requirements
- Performance: Toasts must not cause layout shifts or jank
- Accessibility: All toasts must be keyboard navigable and screen reader accessible
- Scalability: System should handle 100+ concurrent toasts without performance degradation
- Memory efficiency: Toasts should be properly cleaned up from memory after dismissal
- Resilience: System should not crash or break when toast configuration is malformed
Constraints and assumptions
- Toasts will be implemented as React components using React Portal
- Toast system will be global and available throughout the application lifecycle
- Toasts will not persist across page reloads (client-side only)
- We assume a modern browser environment with ES6+ support
- Toasts will be implemented as a singleton pattern to ensure global consistency
Out of scope
- Server-side push notifications (web sockets, FCM)
- Persistent storage of toast history or logs
- Custom animations beyond basic fade transitions
- Drag-and-drop reordering of toasts
- Toast grouping or aggregation features
- Integration with third-party notification services (e.g., Slack, email)
- Mobile-specific touch gestures for toast dismissal
2. Component Architecture
graph TD
A[ToastContainer] --> B[Toast]
A --> C[ToastProvider]
B --> D[ToastContent]
B --> E[ToastActions]
C --> F[useToastHook]
F --> G[ToastStore]
Component Tree and Responsibilities
The toast system is composed of a container component that manages the toast queue, individual toast components that render notifications, and a provider that exposes the toast API to the application.
Smart Components:
ToastContainer- Container that manages toast queue and renders individual toastsToastProvider- Context provider that exposes toast API to the appuseToastHook- Custom hook that provides toast functionality to consumers
Dumb Components:
Toast- Individual toast presentation componentToastContent- Content rendering for toast messagesToastActions- Action buttons (close, retry, etc.)
TypeScript Props Contracts
// ToastProvider.tsx
interface ToastProviderProps {
children: React.ReactNode;
maxToasts?: number;
toastContainerId?: string;
}
// Toast.tsx
interface ToastProps {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title?: string;
description?: string;
duration?: number;
closable?: boolean;
onClose?: () => void;
onAction?: () => void;
actionText?: string;
className?: string;
}
// ToastContent.tsx
interface ToastContentProps {
type: 'success' | 'error' | 'warning' | 'info';
title?: string;
description?: string;
}
// ToastActions.tsx
interface ToastActionsProps {
onAction?: () => void;
onClose?: () => void;
actionText?: string;
closable?: boolean;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Showing : show()
Showing --> Dismissing : autoClose | close()
Dismissing --> Hidden : animationEnd
Showing --> Idle : timeout
Hidden --> [*]
Component Responsibilities
ToastContainer: Manages toast queue, handles positioning and animations, provides context for toastsToastProvider: Wraps app to provide toast API via React Context, manages global stateuseToastHook: Returns toast methods (show, dismiss) and exposes toast queue to consumersToast: Individual toast presentation with animation lifecycle managementToastContent: Renders content based on type and provided propsToastActions: Handles user interactions with toast (close, action button)
Reusability Decisions
The toast system is designed to be highly reusable across the application. It uses React Context for global access, supports multiple toast types, and provides extensible props for customization. The component tree is designed to be modular with clear separation of concerns.
Component Lifecycle Considerations
Toasts have a lifecycle that includes:
- Mounting: Toast is added to queue and rendered
- Showing: Toast animates in and displays content
- Active: Toast is visible to user (with optional auto-close)
- Dismissing: Toast animates out
- Unmounting: Toast is removed from DOM
The system handles:
- Concurrent toasts with unique IDs
- Auto-dismissal based on duration
- Manual dismissal via close button
- Animation state management for smooth transitions
sequenceDiagram
participant App
participant ToastProvider
participant ToastContainer
participant Toast
App->>ToastProvider: Wrap with ToastProvider
ToastProvider->>ToastContainer: Render container
App->>useToastHook: Call toast.show()
useToastHook->>ToastContainer: Add toast to queue
ToastContainer->>Toast: Render new toast
Toast->>Toast: Animate in
Toast->>Toast: Auto-dismiss after duration
Toast->>ToastContainer: Remove from queue
ToastContainer->>Toast: Animate out and remove
3. State Management Design
Local state vs. global state decisions
For a toast/notification system, we need to distinguish between:
- Local state: Per-toast metadata (visibility, timeout handlers)
- Global state: Toast queue management, configuration settings, and system-wide toast behavior
The toast queue and configuration should be global since they affect all toasts in the system. Individual toast metadata is local to each toast component instance.
State management approach
I'll use Zustand as the state management solution because:
- It provides a clean, minimal API similar to React's useState but with global scope
- No provider wrapping needed - simple hooks for consumption
- Excellent TypeScript support with type inference
- Good performance characteristics for this use case
- Easy to test and mock in isolation
Zustand is more appropriate than Redux Toolkit or Context API for this specific use case because:
- We don't need complex middleware or extensive dev tools
- Context API would require a provider wrapper and multiple consumers
- Redux Toolkit introduces unnecessary boilerplate for simple state management
State shape and normalization strategy
// Toast state structure
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
duration?: number;
dismissible?: boolean;
action?: {
label: string;
onClick: () => void;
};
createdAt: number;
}
interface ToastState {
toasts: Toast[];
config: {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
maxToasts?: number;
duration?: number;
};
}
Server state vs. client state separation
In a toast system, all state is client-side:
- Toast queue and configuration are purely client-side concerns
- No server-side state management is required for toast functionality
- Toasts are ephemeral and don't persist across sessions
Cache invalidation strategy
Since toasts are ephemeral and have built-in timeouts, there's no traditional cache invalidation needed. The system manages its own lifecycle through:
- Automatic removal after
durationtimeout - Manual dismissal by user
- Position-based limits (maxToasts)
Optimistic updates design
The toast system supports optimistic updates through:
- Immediate toast display upon API request
- Toast update on success/failure response
- User can dismiss immediately if desired
Critical-path hook implementation
// toastStore.ts
import { create } from 'zustand';
import { v4 as uuidv4 } from 'uuid';
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
duration?: number;
dismissible?: boolean;
action?: {
label: string;
onClick: () => void;
};
createdAt: number;
}
interface ToastConfig {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
maxToasts?: number;
duration?: number;
}
interface ToastStore {
toasts: Toast[];
config: ToastConfig;
addToast: (toast: Omit<Toast, 'id' | 'createdAt'>) => string;
removeToast: (id: string) => void;
updateToast: (id: string, updates: Partial<Toast>) => void;
clearToasts: () => void;
setConfig: (config: Partial<ToastConfig>) => void;
}
const useToastStore = create<ToastStore>((set, get) => ({
toasts: [],
config: {
position: 'top-right',
maxToasts: 5,
duration: 5000,
},
addToast: (toast) => {
const id = uuidv4();
const { config, toasts } = get();
// Apply default values
const toastWithDefaults: Toast = {
id,
message: toast.message,
type: toast.type || 'info',
duration: toast.duration || config.duration,
dismissible: toast.dismissible ?? true,
action: toast.action,
createdAt: Date.now(),
};
// Apply maxToasts limit
const newToasts = [toastWithDefaults, ...toasts].slice(0, config.maxToasts);
set({ toasts: newToasts });
// Auto-remove after duration
if (toastWithDefaults.duration !== Infinity) {
setTimeout(() => {
get().removeToast(id);
}, toastWithDefaults.duration);
}
return id;
},
removeToast: (id) => {
set((state) => ({
toasts: state.toasts.filter(toast => toast.id !== id),
}));
},
updateToast: (id, updates) => {
set((state) => ({
toasts: state.toasts.map(toast =>
toast.id === id ? { ...toast, ...updates } : toast
),
}));
},
clearToasts: () => {
set({ toasts: [] });
},
setConfig: (newConfig) => {
set((state) => ({
config: { ...state.config, ...newConfig },
}));
},
}));
// Custom hook for toast consumption
export const useToast = () => {
const { toasts, config, addToast, removeToast, updateToast, clearToasts } = useToastStore();
const toast = (toast: Omit<Toast, 'id' | 'createdAt'>) => {
return addToast(toast);
};
return {
toasts,
config,
toast,
removeToast,
updateToast,
clearToasts,
setConfig: useToastStore.getState().setConfig,
};
};
// Toast context provider (if needed for SSR or multiple instances)
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div>
{children}
<ToastContainer />
</div>
);
};
// Toast container component that renders toasts
const ToastContainer: React.FC = () => {
const { toasts, config } = useToastStore();
return (
<div
className={`toast-container toast-${config.position}`}
aria-live="polite"
aria-atomic="true"
>
{toasts.map((toast) => (
<Toast key={toast.id} toast={toast} />
))}
</div>
);
};
// Toast component
interface ToastProps {
toast: Toast;
}
const Toast: React.FC<ToastProps> = ({ toast }) => {
const [isVisible, setIsVisible] = useState(true);
const [isHovered, setIsHovered] = useState(false);
const removeToast = useToastStore.getState().removeToast;
useEffect(() => {
if (toast.duration !== Infinity && !isHovered) {
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(() => removeToast(toast.id), 300); // Allow animation time
}, toast.duration);
return () => clearTimeout(timer);
}
}, [toast.duration, isHovered]);
const handleDismiss = () => {
setIsVisible(false);
setTimeout(() => removeToast(toast.id), 300);
};
return (
<div
className={`toast toast-${toast.type} ${isVisible ? 'toast-visible' : 'toast-hidden'}`}
role="alert"
aria-live="assertive"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="toast-content">
{toast.message}
{toast.action && (
<button
className="toast-action"
onClick={toast.action.onClick}
>
{toast.action.label}
</button>
)}
</div>
{toast.dismissible && (
<button
className="toast-dismiss"
onClick={handleDismiss}
aria-label="Close toast"
>
✕
</button>
)}
</div>
);
};
The critical-path hook useToast provides:
- Toast creation with automatic ID generation and timeout handling
- State management for toast queue and configuration
- Lifecycle control through remove/update operations
- Configurable behavior for positioning, max count, and durations
The hook returns a clean API that's easy to consume in any component while maintaining proper state management and lifecycle handling.
4. Routing & Navigation
Route Structure and Hierarchy
The toast/notification system is a global UI feature that doesn't require dedicated routes but needs to be accessible from any part of the application. The system operates as a global singleton component that renders notifications in a fixed position on the screen, typically in the top-right corner.
/
└── [Global Toast Container] (no route needed)
Code Splitting Strategy
Since the toast system is a global UI component with no route-specific code, there's no need for route-based code splitting. The toast container and its associated logic will be imported directly into the root application component.
Justification:
- Toasts are rendered globally and don't depend on route-specific bundles
- The system is lightweight and should be available immediately when the app loads
- No performance benefit from splitting toast-related code
Navigation Guards and Authentication Flows
The toast system itself doesn't participate in navigation guards or authentication flows. However, it should respect authentication state:
- Toasts may be shown to authenticated users only (e.g., success messages after login)
- Error notifications should not reveal sensitive authentication information
- Toasts should be cleared when users log out or navigate away from protected routes
Deep Linking Support and URL Parameter Design
The toast system does not require deep linking support or URL parameter handling. Toasts are ephemeral notifications that don't need to be part of the URL structure.
Browser History Management and Scroll Restoration
The toast system does not interfere with browser history or scroll restoration. Toasts are rendered as overlays that don't affect the DOM structure or scroll position of the underlying content.
// Example toast container integration in app root
import { ToastContainer } from './components/Toast/ToastContainer';
function App() {
return (
<div className="app-root">
{/* Main application content */}
<main>
{children}
</main>
{/* Global toast container - renders at fixed position */}
<ToastContainer />
</div>
);
}
The toast system operates as a singleton component that's always present in the DOM, positioned absolutely or fixed to ensure it appears above all other content regardless of application navigation state.
5. API Integration Layer
API client design pattern
We'll implement a centralized Axios instance with custom configuration for consistent API interactions:
// api/client.ts
import axios, { AxiosInstance } from 'axios';
const apiClient: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for auth token injection
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling and normalization
apiClient.interceptors.response.use(
(response) => response,
(error) => {
// Handle different error types
if (error.response?.status === 401) {
// Handle unauthorized access - redirect to login or trigger auth flow
localStorage.removeItem('authToken');
window.location.href = '/login';
}
// Normalize error response for toast handling
const normalizedError = {
status: error.response?.status,
message: error.response?.data?.message || error.message,
code: error.response?.data?.code,
};
return Promise.reject(normalizedError);
}
);
export default apiClient;
Request/response interceptors
The interceptors handle authentication token injection and standardized error response normalization. They provide centralized error handling that allows the toast system to present consistent user feedback regardless of API error type.
Error handling and retry strategy
For toast integration, we implement a retry strategy with exponential backoff:
// api/retry.ts
export const retryWithBackoff = async (
fn: () => Promise<any>,
retries = 3,
delay = 1000
): Promise<any> => {
try {
return await fn();
} catch (error) {
if (retries > 0 && error.status !== 401) {
await new Promise(resolve => setTimeout(resolve, delay));
return retryWithBackoff(fn, retries - 1, delay * 2);
}
throw error;
}
};
Loading, error, and empty states design
Toast system handles these states through dedicated toast types:
// toast/types.ts
export type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
export interface Toast {
id: string;
type: ToastType;
message: string;
title?: string;
duration?: number;
actions?: ToastAction[];
dismissible?: boolean;
}
export interface ToastAction {
label: string;
onClick: () => void;
}
Data fetching strategy
For toast notifications, we use client-side rendering (CSR) with React Query for optimal caching and revalidation:
// hooks/useToastNotifications.ts
import { useQuery } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useToastNotifications = (userId: string) => {
return useQuery({
queryKey: ['user-notifications', userId],
queryFn: () => apiClient.get(`/users/${userId}/notifications`).then(res => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
};
API response transformation layer
We implement a DTO-to-domain model mapping for consistent toast data:
// api/transformers.ts
import { Toast } from '../toast/types';
export const transformNotificationToToast = (notification: any): Toast => {
return {
id: notification.id,
type: notification.type === 'ERROR' ? 'error' : 'info',
message: notification.message,
title: notification.title,
duration: notification.duration || 5000,
dismissible: notification.dismissible !== false,
};
};
Sequence diagram for primary data flows
sequenceDiagram
participant U as User
participant T as ToastManager
participant A as API Client
participant S as Server
U->>T: Trigger notification (e.g., form submission)
T->>A: Make API request
A->>S: Send request
S-->>A: Response (success/error)
A-->>T: API response with error details
T->>T: Transform to Toast format
T->>U: Display toast notification
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for toast notifications
- Interaction to Next Paint (INP): <200ms for dismiss interactions
- Cumulative Layout Shift (CLS): <0.1 for toast container layout stability
Bundle Size Budget per Route
Toast system should contribute <5KB to total bundle size when minified and gzipped. This includes:
- Toast component implementation (~2KB)
- State management overhead (~1KB)
- Animation utilities (~1.5KB)
- Accessibility helpers (~0.5KB)
Critical Rendering Path Optimization
Toasts must not block rendering of critical content:
- Toast components are rendered in a separate DOM node (portal) outside the main render tree
- CSS is preloaded with
preloadorprefetchto ensure animations don't cause layout shifts - No heavy JavaScript dependencies in toast rendering path
Image Optimization Strategy
While toasts typically don't contain images, any icon assets should:
- Use SVG format for crisp rendering at all sizes
- Be inlined or preloaded with appropriate
loading="lazy"attributes if external - Support dark mode variants through CSS variables or theme switching
Font Loading Strategy
Toast notifications should:
- Use system fonts for fast loading (
-apple-system,BlinkMacSystemFont, etc.) - Include font-display: swap for any custom fonts to prevent FOIT
- Avoid loading heavy font families just for toast text
Memoization Strategy
// Toast component memoization strategy
const Toast = React.memo(({ toast, onDismiss }: ToastProps) => {
// Memoize computed values that don't change per render
const icon = useMemo(() => getToastIcon(toast.type), [toast.type]);
// Avoid unnecessary re-renders by memoizing callback
const handleDismiss = useCallback(() => {
onDismiss(toast.id);
}, [onDismiss, toast.id]);
return (
<div className={`toast toast--${toast.type}`} role="alert">
{icon}
<span>{toast.message}</span>
<button onClick={handleDismiss} aria-label="Dismiss notification">
✕
</button>
</div>
);
});
Virtualization for Large Lists
For scenarios with many concurrent toasts:
// For toast lists exceeding 5-10 items, implement virtualization
const ToastList = ({ toasts }: { toasts: Toast[] }) => {
const { height, width } = useWindowSize();
return (
<div className="toast-container">
{toasts.length > 5 ? (
<FixedSizeList
height={height * 0.8}
width={width * 0.3}
itemCount={toasts.length}
itemSize={60}
>
{({ index, style }) => (
<div style={style}>
<Toast toast={toasts[index]} />
</div>
)}
</FixedSizeList>
) : (
toasts.map(toast => <Toast key={toast.id} toast={toast} />)
)}
</div>
);
};
Prefetching and Preloading Strategy
// Preload toast styles and animations
const preloadToastAssets = () => {
// Preload CSS for toast animations
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/styles/toast-animations.css';
document.head.appendChild(link);
// Preload icon assets if needed
const iconLink = document.createElement('link');
iconLink.rel = 'prefetch';
iconLink.href = '/icons/toast-icons.svg';
document.head.appendChild(iconLink);
};
Animation Performance Considerations
Toasts should use CSS transforms and opacity for animations:
/* Use transform and opacity for hardware acceleration */
.toast {
transition:
transform 0.2s ease-out,
opacity 0.2s ease-out;
}
.toast-enter {
transform: translateY(-100%);
opacity: 0;
}
.toast-enter-active {
transform: translateY(0);
opacity: 1;
}
.toast-exit {
transform: translateY(0);
opacity: 1;
}
.toast-exit-active {
transform: translateY(-100%);
opacity: 0;
}
Memory Management
Toast system should clean up:
- Expired toasts from memory after animation completion
- Event listeners on dismiss buttons
- DOM nodes when toast components unmount
- References to dismissed toast items in state management
Resource Usage Targets
- Toast creation: <5ms per toast
- Toast dismissal animation: <200ms
- Memory footprint: <500 bytes per toast instance
- DOM node count: <100 active toasts max (with overflow handling)
- CPU usage during toast operations: <5% of main thread time
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. For toast notifications, we'll implement error boundaries at the component level where toasts are rendered and at the application level for global error handling.
graph TD
A[App Root] --> B[ToastContainer]
B --> C[ToastList]
C --> D[IndividualToast]
A --> E[ErrorBoundary]
E --> F[GlobalFallbackUI]
Global Error Handler Design
The global error handler will catch unhandled errors and display appropriate toast notifications. It will integrate with React's ErrorBoundary and provide fallback UI for critical failures.
// GlobalErrorHandler.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error | null;
}
class GlobalErrorHandler extends React.Component<{}, ErrorBoundaryState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Log error to monitoring service
console.error('Global Error:', error, errorInfo);
// Show user-friendly toast for critical errors
if (typeof window !== 'undefined') {
// Assuming we have a toast service available globally
toast.error('Something went wrong. Please try again.', {
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
});
}
}
render(): React.ReactNode {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>We're working to fix the issue. Please try again later.</p>
</div>
);
}
return this.props.children;
}
}
export default GlobalErrorHandler;
Offline / Network Failure Handling
The toast system must gracefully handle network failures and offline states:
// OfflineToastManager.ts
import { toast } from 'react-toastify';
class OfflineToastManager {
private offlineToasts: Set<string> = new Set();
showOfflineToast(message: string = 'You are offline') {
const toastId = `offline-${Date.now()}`;
if (this.offlineToasts.has(toastId)) return;
this.offlineToasts.add(toastId);
toast(message, {
toastId,
type: 'warning',
autoClose: false,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
className: 'offline-toast',
});
}
clearOfflineToasts() {
this.offlineToasts.forEach(toastId => {
toast.dismiss(toastId);
});
this.offlineToasts.clear();
}
}
const offlineManager = new OfflineToastManager();
// In your app's network monitoring logic
window.addEventListener('offline', () => {
offlineManager.showOfflineToast();
});
window.addEventListener('online', () => {
offlineManager.clearOfflineToasts();
});
Graceful Degradation Patterns
When toast services are unavailable, the system should fall back to basic UI notifications:
// FallbackToastService.ts
interface ToastOptions {
autoClose?: number | boolean;
hideProgressBar?: boolean;
closeOnClick?: boolean;
pauseOnHover?: boolean;
draggable?: boolean;
type?: 'success' | 'error' | 'info' | 'warning';
}
class FallbackToastService {
private toastContainer: HTMLDivElement | null = null;
constructor() {
this.createToastContainer();
}
private createToastContainer() {
if (typeof document === 'undefined') return;
const container = document.createElement('div');
container.className = 'fallback-toast-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '9999';
document.body.appendChild(container);
this.toastContainer = container;
}
show(message: string, options: ToastOptions = {}) {
if (!this.toastContainer) return;
const toastElement = document.createElement('div');
toastElement.className = `fallback-toast fallback-toast--${options.type || 'info'}`;
toastElement.textContent = message;
// Apply styles based on options
Object.assign(toastElement.style, {
backgroundColor: options.type === 'error' ? '#f44336' :
options.type === 'success' ? '#4caf50' :
options.type === 'warning' ? '#ff9800' : '#2196f3',
color: 'white',
padding: '12px 16px',
borderRadius: '4px',
marginBottom: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
opacity: '0',
transform: 'translateX(100%)',
transition: 'all 0.3s ease-in-out',
});
// Animate in
setTimeout(() => {
toastElement.style.opacity = '1';
toastElement.style.transform = 'translateX(0)';
}, 10);
this.toastContainer.appendChild(toastElement);
// Auto-close if specified
if (options.autoClose !== false) {
const timeout = typeof options.autoClose === 'number' ? options.autoClose : 3000;
setTimeout(() => {
toastElement.style.opacity = '0';
toastElement.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toastElement.parentNode) {
toastElement.parentNode.removeChild(toastElement);
}
}, 300);
}, timeout);
}
// Remove toast after animation completes
setTimeout(() => {
if (toastElement.parentNode) {
toastElement.parentNode.removeChild(toastElement);
}
}, 3500);
}
error(message: string, options?: ToastOptions) {
this.show(message, { ...options, type: 'error' });
}
success(message: string, options?: ToastOptions) {
this.show(message, { ...options, type: 'success' });
}
warning(message: string, options?: ToastOptions) {
this.show(message, { ...options, type: 'warning' });
}
info(message: string, options?: ToastOptions) {
this.show(message, { ...options, type: 'info' });
}
}
// Global fallback instance
const fallbackToastService = new FallbackToastService();
export { fallbackToastService };
User Feedback on Errors
Toasts should provide clear feedback when errors occur:
// ToastWithErrorHandling.tsx
import React from 'react';
import { toast } from 'react-toastify';
interface ToastWithErrorHandlingProps {
title: string;
message: string;
type?: 'success' | 'error' | 'info' | 'warning';
autoClose?: number | boolean;
retryAction?: () => void;
}
const ToastWithErrorHandling: React.FC<ToastWithErrorHandlingProps> = ({
title,
message,
type = 'info',
autoClose = 5000,
retryAction,
}) => {
const handleRetry = () => {
if (retryAction) {
retryAction();
toast.dismiss(); // Dismiss current toast
}
};
const toastConfig = {
type,
autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
};
if (type === 'error' && retryAction) {
toast.error(
<div>
<strong>{title}</strong>
<p>{message}</p>
<button
onClick={handleRetry}
style={{
marginTop: '8px',
padding: '4px 8px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Retry
</button>
</div>,
toastConfig
);
} else {
toast[type](message, toastConfig);
}
};
export default ToastWithErrorHandling;
Error Recovery Mechanisms
Implement retry logic for toast operations:
// RetryableToastService.ts
class RetryableToastService {
private maxRetries = 3;
private retryDelay = 1000;
async showWithRetry(
toastFn: () => Promise<void>,
maxRetries?: number,
delay?: number
): Promise<void> {
const retries = maxRetries || this.maxRetries;
const delayMs = delay || this.retryDelay;
for (let i = 0; i <= retries; i++) {
try {
await toastFn();
return;
} catch (error) {
if (i === retries) {
// Final attempt failed, show fallback
console.error('Toast operation failed after all retries:', error);
toast.error('Failed to show notification. Please try again.');
return;
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delayMs * Math.pow(2, i)));
}
}
}
async showSuccessWithRetry(message: string) {
await this.showWithRetry(() => toast.success(message));
}
async showErrorWithRetry(message: string) {
await this.showWithRetry(() => toast.error(message));
}
}
const retryableToastService = new RetryableToastService();
export { retryableToastService };
8. Testing Strategy
Unit Testing Approach
Component-level testing (70% coverage target):
- Toast component rendering with different types (success, error, warning, info)
- Toast lifecycle management (auto-dismiss, manual dismiss)
- Toast content rendering with various message formats
- Toast positioning and animation effects
- Focus management for accessibility compliance
Utility function testing (80% coverage target):
- Toast queue management logic
- Toast ID generation and collision handling
- Toast type validation and transformation
- Dismiss timeout calculation and cleanup
Mocking strategy:
// Mock DOM APIs for animation testing
const mockAnimation = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
play: jest.fn(),
pause: jest.fn()
};
Object.defineProperty(global, 'Animation', {
writable: false,
value: jest.fn().mockImplementation(() => mockAnimation)
});
Integration Testing Strategy
Toast manager integration:
- End-to-end toast creation and dismissal flow
- Concurrent toast queue behavior (max concurrent limit)
- Toast stacking and positioning calculations
- State synchronization between multiple toast instances
Cross-component integration:
- Toast display with API error handling
- Toast visibility with route changes
- Toast persistence during app state transitions
E2E Testing Scope
Cypress test coverage (target: 85% of user flows):
- Basic toast display and dismissal
- Toast queue overflow handling (max 5 toasts)
- Toast persistence across navigation
- Keyboard accessibility testing (Tab/Enter/Escape)
- Screen reader compatibility verification
Critical user journeys:
- API error triggers toast display
- User dismisses toast manually
- Toast auto-dismisses after timeout
- Multiple concurrent toasts display correctly
Visual Regression Testing
Screenshot testing strategy:
- All toast types (success, error, warning, info) in default and custom positions
- Toast stacking scenarios with 3+ concurrent toasts
- RTL layout variations for internationalized content
- Responsive breakpoints (mobile, tablet, desktop)
Accessibility Testing Automation
axe-core integration:
import { axe } from 'axe-core';
test('Toast component passes accessibility checks', async () => {
const { container } = render(<Toast type="success" message="Success!" />);
const results = await axe.run(container);
expect(results.violations).toHaveLength(0);
});
Keyboard navigation coverage:
- Tab navigation through toast controls
- Enter/Space key for dismiss action
- Escape key for immediate dismissal
- Focus trapping within toast container
Test Data Strategy
Test fixtures:
// test/fixtures/toast-fixtures.ts
export const toastFixtures = {
success: {
type: 'success',
message: 'Operation completed successfully',
autoDismiss: true,
timeout: 5000
},
error: {
type: 'error',
message: 'Failed to save changes',
autoDismiss: false,
timeout: 0
},
warning: {
type: 'warning',
message: 'Please review your input',
autoDismiss: true,
timeout: 8000
}
};
Test data diversity:
- Short and long message content (100+ character limits)
- Special characters and emoji support
- Multiline message rendering
- RTL language content (Arabic, Hebrew)
- Empty and null state handling
Test data generation:
- Random ID generation for toast uniqueness
- Timestamp-based auto-dismiss calculation
- Position coordinate validation (top-left, top-right, etc.)
- Concurrent queue state management testing
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The toast/notification system must meet the following WCAG 2.1 AA requirements:
- 1.1.1 Non-text Content: All toast icons must have appropriate ARIA labels or be announced by screen readers
- 1.3.1 Info and Relationships: Toasts must maintain proper semantic structure for screen readers
- 1.4.1 Use of Color: Toasts must be distinguishable by color contrast (4.5:1 for normal text, 3:1 for large text)
- 1.4.3 Contrast (Minimum): Toasts must meet minimum contrast ratios for text and backgrounds
- 1.4.12 Text Spacing: Toasts must support text spacing adjustments without content overflow
- 1.4.13 Content on Hover/Focus: Toasts must not rely solely on hover/focus for critical information
- 1.4.13 Content on Hover/Focus: Toasts must not disappear automatically without user control
- 2.1.1 Keyboard: All toast interactions must be keyboard accessible (close button, focus management)
- 2.4.1 Bypass Blocks: Toasts must not interfere with keyboard navigation flow
- 3.1.1 Language: Toast content language must be properly marked if different from default
- 4.1.2 Name, Role, Value: Toast DOM elements must have proper ARIA roles and states
Keyboard navigation support
All toast interactions must be fully keyboard accessible:
- Focus Management: Toasts should receive focus when they appear, and focus should be trapped within the toast until dismissed
- Tab Navigation: Users must be able to tab through toast elements (close button, action buttons)
- Escape Key: Pressing Escape should dismiss the toast and return focus to the previous element
- Close Button: Close button must be keyboard accessible with proper ARIA attributes
Focus management strategy
// ToastManager component focus management
const useToastFocusManagement = () => {
const [activeToastId, setActiveToastId] = useState<string | null>(null);
const handleToastOpen = (toastId: string) => {
setActiveToastId(toastId);
// Focus the toast container for screen readers
setTimeout(() => {
const toastElement = document.getElementById(`toast-${toastId}`);
if (toastElement) {
toastElement.focus();
}
}, 0);
};
const handleToastClose = () => {
setActiveToastId(null);
// Return focus to previous active element
const previousActiveElement = document.activeElement as HTMLElement;
if (previousActiveElement) {
previousActiveElement.focus();
}
};
return { activeToastId, handleToastOpen, handleToastClose };
};
ARIA roles and attributes plan
Each toast must implement appropriate ARIA attributes:
// Toast component ARIA implementation
interface ToastProps {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
title?: string;
autoClose?: boolean;
duration?: number;
onClose: () => void;
}
const Toast = ({
id,
type,
message,
title,
autoClose = true,
duration = 5000,
onClose
}: ToastProps) => {
const [isVisible, setIsVisible] = useState(true);
// ARIA roles and attributes
const role = type === 'error' ? 'alert' : 'status';
const ariaLive = type === 'error' ? 'assertive' : 'polite';
const ariaLabel = type === 'error' ? 'Error notification' :
type === 'success' ? 'Success notification' :
type === 'warning' ? 'Warning notification' :
'Information notification';
return (
<div
id={`toast-${id}`}
role={role}
aria-live={ariaLive}
aria-atomic="true"
aria-label={ariaLabel}
className={`toast toast--${type}`}
tabIndex={0} // Make toast focusable
onFocus={() => {
// Pause auto-close when focused
if (autoClose) {
// Implementation for pause logic
}
}}
onBlur={() => {
// Resume auto-close when focus lost
if (autoClose) {
// Implementation for resume logic
}
}}
>
<div className="toast__content">
{title && (
<h3
className="toast__title"
aria-label={title}
>
{title}
</h3>
)}
<p
className="toast__message"
aria-label={message}
>
{message}
</p>
</div>
<button
className="toast__close-button"
aria-label="Close notification"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onClose();
}
}}
>
<span aria-hidden="true">×</span>
</button>
</div>
);
};
Screen reader compatibility
- Announcement Priority: Error toasts should be announced with
aria-live="assertive"while info/warning toasts usearia-live="polite" - Content Announcements: Toast content should be announced immediately upon appearance
- Dynamic Updates: When toast content changes dynamically, proper ARIA updates must be handled
- Focus Trapping: Screen reader focus should remain within the toast until dismissed
Additional accessibility considerations
- Announcement Delay: Toasts should not interrupt ongoing screen reader announcements
- Multiple Toast Support: When multiple toasts appear, they should be announced in order of appearance
- Custom Content Handling: Toasts with custom content must ensure proper semantic structure for screen readers
- Time-based Content: Auto-closing toasts should provide appropriate time context for screen reader users
- Visual vs. Aural: Ensure that visually apparent information (like color coding) is also conveyed through screen reader announcements
Keyboard accessibility patterns
// Toast keyboard navigation handler
const useToastKeyboardNavigation = () => {
const handleKeyDown = (e: KeyboardEvent, toastId: string) => {
if (e.key === 'Escape') {
// Dismiss toast and restore focus
const toastElement = document.getElementById(`toast-${toastId}`);
if (toastElement) {
toastElement.blur();
}
// Call dismiss handler
}
if (e.key === 'Tab') {
// Handle tab navigation within toast
const closeBtn = document.querySelector(`[aria-label="Close notification"]`);
if (closeBtn && document.activeElement === closeBtn) {
// Move focus to first element after toast or return to previous focus
}
}
};
return { handleKeyDown };
};
The system must support all keyboard navigation patterns expected by users with disabilities and provide equivalent functionality to mouse-based interactions.
10. Security Considerations
XSS Prevention Strategy
The toast system must prevent cross-site scripting attacks through strict input sanitization and output encoding:
// Secure toast content handling
const sanitizeToastContent = (content: string): string => {
const div = document.createElement('div');
div.textContent = content;
return div.innerHTML;
};
// Example of secure toast creation
const createSecureToast = (message: string, type: ToastType) => {
const sanitizedMessage = sanitizeToastContent(message);
return {
id: generateId(),
message: sanitizedMessage,
type,
timestamp: Date.now()
};
};
CSRF Protection
While toasts themselves don't typically handle authentication, the toast system should not expose sensitive data that could be exploited:
- Toasts should never display raw API responses or internal error stack traces
- All toast messages must be sanitized and user-friendly before display
- No sensitive session or authentication tokens should appear in toast content
Content Security Policy (CSP) Headers
The frontend must respect CSP policies to prevent script injection in toast content:
// CSP policy enforcement for toast content
const validateToastContent = (content: string): boolean => {
// Check for dangerous patterns that could indicate XSS
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/on\w+\s*=\s*["'][^"']*["']/gi,
/javascript:/gi
];
return !dangerousPatterns.some(pattern => pattern.test(content));
};
Sensitive Data Handling in Frontend State
Toast messages should never contain sensitive information:
// Toast state management with sensitive data filtering
interface ToastState {
id: string;
message: string; // Sanitized user-facing message
type: 'success' | 'error' | 'warning' | 'info';
timestamp: number;
duration?: number;
dismissible?: boolean;
}
// Filter sensitive data from toast content
const filterSensitiveData = (message: string): string => {
// Remove any potential sensitive data patterns
return message.replace(/\b(?:password|token|secret|api_key)\b/gi, '[REDACTED]');
};
Input Sanitization Approach
All toast content must undergo strict sanitization:
// Comprehensive input sanitization for toast messages
const sanitizeToastInput = (input: string): string => {
// HTML escape all special characters
const escaped = input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// Remove any remaining dangerous HTML tags
return escaped.replace(/<[^>]*>/g, '');
};
// Usage in toast creation
const createToast = (message: string, type: ToastType) => {
const sanitizedMessage = sanitizeToastInput(message);
return {
id: generateId(),
message: sanitizedMessage,
type,
timestamp: Date.now()
};
};
Security Best Practices Implementation
- Never trust user input - All toast messages must be sanitized before display
- Validate content length - Prevent oversized content that could cause rendering issues
- Implement proper escaping - HTML and script tag escaping for all content
- Restrict DOM manipulation - Toasts should not allow arbitrary DOM injection
- Secure ID generation - Use cryptographically secure IDs to prevent ID guessing attacks
Attack Surface Reduction
The toast system should not:
- Accept raw HTML content from external sources
- Display API error responses directly to users
- Allow arbitrary JavaScript execution through toast content
- Expose internal system information in user-facing messages
All toast content should be processed through a security filter that ensures only safe, user-friendly text is displayed to end users.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following BEM-style naming with feature-scoped keys to prevent collisions:
// Translation keys structure
{
"toast": {
"success": "Success",
"error": "Error",
"warning": "Warning",
"info": "Info",
"dismiss": "Dismiss",
"defaultTitle": "Notification",
"defaultDescription": "Your notification message here"
},
"toast:success:created": "Item created successfully",
"toast:error:network": "Network error occurred",
"toast:warning:timeout": "Request timeout exceeded"
}
RTL Support Considerations
Toast notifications will automatically adapt to RTL layout using CSS logical properties:
/* Toast container with RTL support */
.toast-container {
/* Will automatically adjust based on text direction */
left: auto;
right: auto;
padding-inline-start: 1rem;
padding-inline-end: 1rem;
}
/* RTL-aware positioning */
[dir="rtl"] .toast-container {
left: auto;
right: 1rem;
}
Date, Number, Currency Formatting Strategy
Toast messages may include time-based content that requires proper localization:
// Example of localized timestamp in toast
const formatToastTimestamp = (date: Date, locale: string) => {
return new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
};
// Example of localized numeric values in toast
const formatToastCount = (count: number, locale: string) => {
return new Intl.NumberFormat(locale).format(count);
};
Locale Detection and Switching Strategy
Automatic locale detection using browser navigator:
// Auto-detect user's preferred language
const detectUserLocale = (): string => {
const browserLang = navigator.language || navigator.languages[0];
const supportedLocales = ['en', 'es', 'fr', 'de', 'ja'];
// Match to supported locales or default to English
return supportedLocales.includes(browserLang)
? browserLang
: 'en';
};
// Manual locale switching with persistence
const switchLocale = (newLocale: string) => {
localStorage.setItem('preferred-locale', newLocale);
// Trigger re-render of all localized content
window.location.reload();
};
Translation Key Management
Implementation uses a centralized translation key registry:
// Translation keys enum for type safety
export enum ToastTranslationKeys {
SUCCESS = 'toast.success',
ERROR = 'toast.error',
WARNING = 'toast.warning',
INFO = 'toast.info',
DISMISS = 'toast.dismiss',
DEFAULT_TITLE = 'toast.defaultTitle',
DEFAULT_DESCRIPTION = 'toast.defaultDescription'
}
// Toast-specific translation keys
export enum ToastMessageKeys {
CREATED = 'toast:success:created',
NETWORK_ERROR = 'toast:error:network',
TIMEOUT_WARNING = 'toast:warning:timeout'
}
i18n Integration Points
Toast system will integrate with existing i18n provider through context:
// Context for providing translation functions to toast components
interface ToastI18nContext {
t: (key: string, params?: Record<string, any>) => string;
locale: string;
}
const ToastI18nContext = createContext<ToastI18nContext>({
t: (key) => key,
locale: 'en'
});
Message Template Handling
Support for parameterized messages with proper pluralization:
// Example of localized message with parameters
const getLocalizedMessage = (
key: string,
params?: { count?: number; name?: string },
locale: string = 'en'
): string => {
// Fetch translation with interpolation
const baseMessage = t(key, params);
// Handle pluralization for count-based messages
if (params?.count !== undefined) {
const pluralForm = getPluralForm(params.count, locale);
return baseMessage.replace('{count}', params.count.toString());
}
return baseMessage;
};
Content Direction Adaptation
Toast components will adapt to content direction:
// Component that respects text direction
const ToastContent = ({
message,
direction = 'ltr'
}: {
message: string;
direction?: 'ltr' | 'rtl'
}) => {
return (
<div
className="toast-content"
dir={direction}
style={{
textAlign: direction === 'rtl' ? 'right' : 'left'
}}
>
{message}
</div>
);
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The toast system should capture key performance indicators to understand user experience:
- Toast Display Time: How long users see toasts before they disappear or are dismissed
- Toast Success Rate: Percentage of toasts that successfully display vs. failed displays
- User Interaction Rate: % of toasts that receive user action (click, dismiss) vs. auto-dismissal
- Toast Queue Latency: Time from toast creation to actual display in UI queue
- Toast Failure Rate: % of toast creation attempts that fail due to system constraints
Error Tracking Integration
Integrate with error monitoring platforms (Sentry, Datadog) to capture:
- Toast rendering failures (DOM errors, context provider issues)
- API integration errors when toast content depends on external data
- Memory leak patterns in toast queue management
- Race conditions in concurrent toast creation
Custom Events and User Journey Tracking
Implement event tracking for key user interactions:
// Toast event tracking interface
interface ToastEvent {
eventType: 'toast_displayed' | 'toast_dismissed' | 'toast_action_clicked' | 'toast_error';
toastId: string;
timestamp: number;
userId?: string;
context?: Record<string, any>;
}
// Example implementation in toast manager
const trackToastEvent = (event: ToastEvent) => {
// Send to analytics service or RUM provider
if (window?.analytics) {
window.analytics.track('toast_event', event);
}
};
Performance Monitoring and Regression Detection
Monitor toast system performance to detect regressions:
- Memory Usage: Track toast queue size and DOM node count over time
- GC Pressure: Monitor garbage collection frequency when many toasts are active
- Render Performance: Measure toast component render times and frame drops
- Queue Processing Time: Track time to process new toast additions to queue
Implement performance monitoring hooks:
// Performance monitoring for toast system
const useToastPerformance = () => {
const [metrics, setMetrics] = useState({
queueSize: 0,
renderTime: 0,
memoryUsage: 0
});
useEffect(() => {
// Periodic performance sampling
const interval = setInterval(() => {
const queueSize = toastQueue.length;
const memoryUsage = getMemoryUsage(); // Pseudo-code for memory measurement
setMetrics({
queueSize,
renderTime: calculateRenderTime(),
memoryUsage
});
// Send to performance monitoring service
if (window?.performance) {
window.performance.mark('toast_metrics_update');
}
}, 5000);
return () => clearInterval(interval);
}, []);
return metrics;
};
Alerting and Thresholds
Configure alerting thresholds for toast system health:
- Queue Size Alert: > 20 toasts in queue triggers alert
- Render Time Alert: > 100ms per toast render triggers performance alert
- Failure Rate Alert: > 5% toast creation failures trigger error alert
- Memory Leak Alert: Memory usage increasing by > 1MB over 5 minutes
Data Collection Strategy
Implement data collection with privacy considerations:
// Privacy-aware telemetry configuration
const toastTelemetryConfig = {
enabled: true,
collectUserIds: false,
sampleRate: 0.1, // 10% of sessions
anonymize: true,
maxQueueSize: 50,
retentionDays: 30
};
Dashboard and Visualization
Create dashboards to visualize toast system health:
- Toast Volume Dashboard: Real-time toast creation rate
- Queue Health Dashboard: Queue size, processing latency
- User Interaction Dashboard: Dismissal rates, action click-through rates
- Error Rate Dashboard: Failure and error occurrence trends
Integration with Existing Monitoring Infrastructure
The toast monitoring system should integrate with existing observability tools:
- Log Aggregation: Toast events logged to centralized logging system
- Metric Collection: Prometheus/Grafana compatible metrics for toast performance
- Trace Integration: Correlate toast events with backend request traces
- Alerting Integration: Connect to existing alerting systems (PagerDuty, Slack)
Data Retention and Compliance
Implement proper data handling:
- Compliance: Ensure no PII in toast telemetry
- Retention Policy: 30-day retention for performance data, 7-day for error logs
- Data Export: Support for data export to external monitoring systems
- Privacy Controls: Allow users to opt-out of telemetry collection
Health Checks and Self-Monitoring
Implement built-in health checks:
// Toast system health check
const toastHealthCheck = () => {
const health = {
queueHealthy: toastQueue.length < 50,
memoryHealthy: getMemoryUsage() < 100 * 1024 * 1024, // 100MB
renderHealthy: calculateRenderTime() < 50,
errorRate: getErrorRate(),
uptime: Date.now() - systemStartTime
};
return health;
};
Rollback and Recovery Mechanisms
Implement monitoring for system recovery:
- Graceful Degradation: When toast system fails, fall back to basic console logging
- Recovery Metrics: Track time to recovery from toast system failures
- Fallback Behavior: Monitor when fallback logging is triggered due to toast failure
13. File & Folder Structure
Proposed Directory Structure
src/
├── components/
│ └── Toast/
│ ├── Toast.tsx
│ ├── ToastContainer.tsx
│ ├── ToastProvider.tsx
│ ├── toastTypes.ts
│ └── index.ts
├── hooks/
│ └── useToast.ts
├── contexts/
│ └── ToastContext.ts
├── types/
│ └── toast.ts
└── utils/
└── toastUtils.ts
Module Boundaries and Import Rules
- ToastProvider and ToastContext are exported from
components/Toast/index.tsto provide a clean API for consumers - useToast hook is exported from
hooks/index.tsto maintain separation of concerns between presentation and logic - toastTypes.ts and toastUtils.ts are internal utilities that can be imported by any component in the toast module but not by other modules
- No cross-feature imports are allowed to maintain modular boundaries
Shared vs Feature-Specific Code Separation
The toast system is designed as a shared component that can be used across the application without feature-specific logic. It's built to be generic enough that it can handle various toast types (success, error, warning, info) and configurations while maintaining clean separation from business logic. The system is designed to be imported and used by any feature module without tight coupling.
The structure follows a feature-based organization where the toast system is contained within its own module, making it easy to test in isolation and maintain as a self-contained unit.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React (v18+)
- Justification: Core library for component-based UI development, essential for building toast components with proper lifecycle management and state handling.
- Bundle Impact: ~150KB (minified) - unavoidable dependency for React applications.
2. react-transition-group (v4+)
- Justification: Provides robust animation and transition handling for toast appearance/disappearance with proper DOM cleanup. Essential for smooth user experience without relying on CSS animations that might conflict.
- Bundle Impact: ~15KB (minified) - lightweight and purpose-built for this use case.
3. clsx (v2+)
- Justification: Utility for conditionally joining CSS class names, enabling clean and readable styling logic without complex ternary operators or string concatenation.
- Bundle Impact: ~1KB (minified) - minimal overhead with maximum utility.
4. @types/react (v18+)
- Justification: TypeScript type definitions for React, ensuring type safety and better developer experience in toast component development.
- Bundle Impact: No runtime impact - only type definitions.
Alternatives Considered and Rejected
1. CSS-in-JS Libraries (styled-components, emotion)
- Rejected: Overkill for toast system which requires minimal styling. Adds significant bundle size (200-500KB) and complexity for simple UI elements.
- Trade-off: Bundle size vs. developer experience.
2. Framer Motion
- Rejected: While powerful for animations, toast system only needs simple enter/exit transitions. Framer Motion adds ~20KB+ bundle size and introduces unnecessary complexity for basic animation needs.
- Trade-off: Animation flexibility vs. performance.
3. Material UI Toast Components
- Rejected: Would introduce unnecessary dependencies and styling conflicts with existing design systems. Toasts are a minimal UI element that should be lightweight and customizable.
- Trade-off: Pre-built functionality vs. customizability.
Licensing and Bundle Size Impact Assessment
Bundle Size Budget Allocation:
- Core React + react-transition-group + clsx = ~165KB total (minimal impact on overall bundle size)
- All dependencies are MIT licensed with no restrictions for production use
- No third-party runtime dependencies beyond React ecosystem
Performance Impact:
- All selected libraries are lightweight and purpose-built
- No runtime overhead for toast functionality beyond what's necessary
- Bundle size increase is negligible (under 0.5% of typical React application bundle)
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Toast stacking behavior: Should toasts stack vertically or replace older ones? What's the maximum concurrent toast limit?
- Auto-dismiss timing: What default durations should be used for different toast types (success: 5s, error: 10s, info: 30s)?
- User interaction handling: Should toasts be dismissible by user click or only auto-dismiss? How should hover/pause behavior work?
- Toast persistence: Should critical errors or important notifications persist until user action rather than auto-dismiss?
Known trade-offs made in this design
- Simplicity vs. Feature Richness: Prioritized clean, minimal API over extensive customization options to avoid feature bloat and maintain performance
- Global State vs. Scoped State: Used global state management for toast visibility but isolated component rendering to prevent cascading re-renders
- Animation Performance vs. Visual Appeal: Chose simple CSS transitions over complex animations to maintain optimal Core Web Vitals scores
- Type Safety vs. Flexibility: Strong typing for toast content but limited generic payload support to maintain type safety and prevent runtime errors
Risks and mitigations
- Performance Degradation: Risk of excessive DOM nodes from too many concurrent toasts. Mitigation: Implement max toast limit with queueing strategy
- Accessibility Issues: Risk of screen reader conflicts or keyboard navigation problems. Mitigation: Comprehensive ARIA implementation and thorough accessibility testing
- State Management Conflicts: Risk of toast state conflicts in complex app with multiple notification sources. Mitigation: Centralized queue system with unique IDs and proper cleanup
- Memory Leaks: Risk of toast references persisting after dismissal. Mitigation: Proper cleanup in useEffect and component unmount handlers
Technical debt acknowledged
- Limited Customization: The current design doesn't support extensive theme customization or complex content rendering, which may require future refactoring
- Static Positioning: Toasts are currently hard-coded to top-right position, limiting flexibility for different app layouts
- Basic Error Recovery: Only basic retry logic for failed toast creation, no sophisticated error recovery patterns implemented
- No Toast Grouping: Individual toasts are not grouped or batched, which could impact performance in high-volume scenarios
- Limited Internationalization Support: Toast content is not fully prepared for i18n with dynamic message interpolation, requiring future enhancements