This document presents a production-ready modal dialog implementation focusing on accessibility, performance, and extensibility for modern web applications.
1. Problem Statement & Requirements
Clarifying questions
- What target browsers/devices are we supporting? (e.g., IE11, mobile Safari, Chrome 80+)
- Should this modal support real-time updates or is it primarily static content?
- Do we need to support nested modals or stacking behavior?
- Is there a specific design system or component library we must integrate with?
- Should this modal support keyboard navigation and screen reader accessibility out of the box?
- Do we need to support internationalization (i18n) with RTL layout support?
- Are there any specific performance constraints or bundle size budgets?
Functional requirements
- Display content in a centered overlay modal with backdrop
- Support for multiple modal sizes (small, medium, large, full-screen)
- Close functionality via X button, Escape key, or clicking backdrop
- Support for modal header, body, and footer sections
- Optional close button in header
- Support for modal title and description text
- Keyboard navigation support (focus trapping)
- Accessibility compliance with ARIA standards
Non-functional requirements
- Performance: Modal should not cause layout shifts or jank on open/close
- Accessibility: Must meet WCAG 2.1 AA standards for keyboard navigation and screen readers
- Scalability: Should handle large content without performance degradation
- Maintainability: Component should be easily customizable and extensible
Constraints and assumptions
- Modal will be rendered in a React application using modern hooks
- Target browsers support modern JavaScript and CSS features (CSS variables, flexbox)
- No third-party modal libraries will be used - custom implementation required
- Component will be built with TypeScript for type safety
- Focus management and keyboard handling will be implemented manually
Out of scope
- Modal stacking or nested modal support (single modal at a time)
- Animation libraries or complex transitions (basic CSS transitions only)
- Server-side rendering integration
- Custom modal themes beyond size variants
- Drag-to-close functionality
- Modal persistence across page reloads
2. Component Architecture
graph TD
A[Modal] --> B[ModalBackdrop]
A --> C[ModalContainer]
C --> D[ModalHeader]
C --> E[ModalBody]
C --> F[ModalFooter]
A --> G[ModalCloseButton]
subgraph ModalComponents
B
C
D
E
F
G
end
Component Tree Breakdown
Smart Components (Container/Logic):
Modal- Main container with state management and accessibility handlingModalManager- Global modal registry and keyboard event handler (optional)
Dumb Components (Presentational):
ModalBackdrop- Semi-transparent overlay backgroundModalContainer- Main modal container with positioning and animationModalHeader- Title and close button containerModalBody- Content area with scroll handlingModalFooter- Action buttons containerModalCloseButton- Close button with accessibility attributes
TypeScript Props Contracts
// Modal component props
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
className?: string;
ariaLabelledBy?: string;
ariaDescribedBy?: string;
}
// ModalHeader props
interface ModalHeaderProps {
title: string;
onClose: () => void;
showCloseButton?: boolean;
className?: string;
}
// ModalBody props
interface ModalBodyProps {
children: React.ReactNode;
className?: string;
}
// ModalFooter props
interface ModalFooterProps {
children: React.ReactNode;
className?: string;
}
UI State Machine
stateDiagram-v2
[*] --> Closed
Closed --> Opening : open()
Opening --> Opened : animationComplete
Opened --> Closing : close()
Closing --> Closed : animationComplete
Opened --> Closing : overlayClick()
Opened --> Closing : escapeKey()
Component Responsibilities
- Modal: Main orchestrator that manages open/closed state, accessibility attributes, keyboard handling, and animation lifecycle
- ModalBackdrop: Renders semi-transparent overlay with proper z-index and click handling
- ModalContainer: Handles positioning, sizing, and animation transitions
- ModalHeader: Contains title and close button with proper ARIA attributes
- ModalBody: Content area with scroll handling and overflow management
- ModalFooter: Action buttons container with proper spacing and alignment
Reusability Decisions
The Modal component is designed to be highly reusable with:
- Extensible props system for customization
- Component composition pattern (Header, Body, Footer)
- CSS-in-JS or CSS modules for theming support
- TypeScript interfaces for type safety across implementations
Component Lifecycle Considerations
- Mount: Focus trap initialization, keyboard event listeners registration
- Update: State changes trigger animation transitions and accessibility updates
- Unmount: Cleanup of event listeners, focus restoration, cleanup of DOM modifications
// Example usage pattern for component composition
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalHeader title="User Profile" onClose={handleClose} />
<ModalBody>
<UserProfileForm />
</ModalBody>
<ModalFooter>
<Button onClick={handleSave}>Save</Button>
<Button variant="secondary" onClick={handleClose}>Cancel</Button>
</ModalFooter>
</Modal>
3. State Management Design
Local state vs. global state decisions
For a Modal/Dialog component, state management strategy depends on the modal's scope and usage patterns:
- Local state: Modal open/closed status, animation states, focus trap state - these are inherently tied to a single modal instance and don't need to be shared across the app.
- Global state: Modal queue management, backdrop stacking, keyboard navigation context - these are needed across multiple modals and should be managed at a global level.
State management approach
React Context API + Zustand for complex state
For this design, I'll use a hybrid approach:
- Context API for global modal state (stacking order, backdrop management)
- Zustand for local modal state and complex interactions (focus trapping, animation states)
This approach provides:
- Clean separation of concerns - global state is isolated from component logic
- Minimal re-renders through proper selector usage
- Easy testing and mocking capabilities
- Scalable for future expansion
State shape and normalization strategy
// Modal state structure
interface ModalState {
id: string;
isOpen: boolean;
isAnimating: boolean;
zIndex: number;
backdropId?: string;
content: React.ReactNode | string;
options: ModalOptions;
}
interface ModalQueue {
modals: ModalState[];
activeModalId?: string;
}
interface ModalOptions {
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
showBackdrop?: boolean;
backdropOpacity?: number;
animationDuration?: number;
className?: string;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
dismissible?: boolean;
}
// Global modal store
interface ModalStore {
queue: ModalQueue;
openModals: Set<string>;
activeModalId: string | null;
backdropStack: Set<string>;
// Actions
openModal: (modalId: string, options?: ModalOptions) => void;
closeModal: (modalId: string) => void;
updateModal: (modalId: string, updates: Partial<ModalState>) => void;
setBackdrop: (backdropId: string) => void;
removeBackdrop: (backdropId: string) => void;
}
Server state vs. client state separation
- Client state: Modal open/closed status, animation states, focus management
- Server state: Content data that populates modal (fetched via API)
- Hybrid approach: Modal state is client-managed, content is server-fetched
Cache invalidation strategy
Since modals don't typically hold cached data, the main concern is content refresh:
- Modal content should be refetched when
isOpenchanges and content requires fresh data - Use React Query's
refetchOnMountorrefetchOnWindowFocusfor content refresh - Implement cache key strategy based on modal type and content ID
Optimistic updates design
Modals don't typically involve optimistic updates as they're primarily read-only UI elements. However, when modals are used for form submissions:
// Example optimistic update pattern in modal context
const handleFormSubmit = async (formData: FormData) => {
const optimisticUpdate = createOptimisticUpdate(formData);
// Update local state immediately
updateModalContent(optimisticUpdate);
try {
const result = await api.submitForm(formData);
// Update modal with actual result
updateModalContent(result);
// Close modal on success
closeModal(modalId);
} catch (error) {
// Revert optimistic update on failure
updateModalContent(originalContent);
showErrorMessage(error);
}
};
Critical-path hook implementation
// useModal.ts - Primary custom hook for modal state management
import { create } from 'zustand';
import { useEffect, useCallback, useRef } from 'react';
interface ModalState {
id: string;
isOpen: boolean;
isAnimating: boolean;
zIndex: number;
content: React.ReactNode | string;
options: ModalOptions;
}
interface ModalStore {
modals: Record<string, ModalState>;
activeModalId: string | null;
backdropStack: Set<string>;
// Actions
openModal: (id: string, content: React.ReactNode | string, options?: ModalOptions) => void;
closeModal: (id: string) => void;
updateModal: (id: string, updates: Partial<ModalState>) => void;
setActiveModal: (id: string | null) => void;
setBackdrop: (backdropId: string) => void;
removeBackdrop: (backdropId: string) => void;
}
const useModalStore = create<ModalStore>((set, get) => ({
modals: {},
activeModalId: null,
backdropStack: new Set(),
openModal: (id, content, options = {}) => {
set((state) => {
const zIndex = state.backdropStack.size * 10 + 1000;
return {
modals: {
...state.modals,
[id]: {
id,
isOpen: true,
isAnimating: false,
zIndex,
content,
options: {
closeOnOverlayClick: true,
closeOnEscape: true,
showBackdrop: true,
backdropOpacity: 0.5,
animationDuration: 300,
size: 'md',
dismissible: true,
...options
}
}
},
activeModalId: id
};
});
},
closeModal: (id) => {
set((state) => {
const modals = { ...state.modals };
delete modals[id];
return {
modals,
activeModalId: state.activeModalId === id ? null : state.activeModalId
};
});
},
updateModal: (id, updates) => {
set((state) => ({
modals: {
...state.modals,
[id]: {
...state.modals[id],
...updates
}
}
}));
},
setActiveModal: (id) => set({ activeModalId: id }),
setBackdrop: (backdropId) => {
set((state) => ({
backdropStack: new Set(state.backdropStack).add(backdropId)
}));
},
removeBackdrop: (backdropId) => {
set((state) => {
const backdropStack = new Set(state.backdropStack);
backdropStack.delete(backdropId);
return { backdropStack };
});
}
}));
// Custom hook for modal interaction
export const useModal = (modalId: string) => {
const {
modals,
activeModalId,
backdropStack,
openModal,
closeModal,
updateModal,
setActiveModal,
setBackdrop,
removeBackdrop
} = useModalStore();
const modalState = modals[modalId];
// Handle keyboard events
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && activeModalId === modalId) {
const modal = modals[modalId];
if (modal?.options.closeOnEscape) {
closeModal(modalId);
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [activeModalId, modalId, modals]);
// Handle backdrop stacking
useEffect(() => {
if (modalState?.isOpen) {
setBackdrop(modalId);
}
return () => {
if (modalState?.isOpen) {
removeBackdrop(modalId);
}
};
}, [modalState?.isOpen, modalId]);
// Animation state management
const animateOpen = useCallback(() => {
updateModal(modalId, { isAnimating: true });
setTimeout(() => {
updateModal(modalId, { isAnimating: false });
}, modalState?.options.animationDuration || 300);
}, [modalId, modalState?.options.animationDuration]);
const handleOpen = useCallback(() => {
openModal(modalId, '', {});
animateOpen();
}, [modalId, openModal, animateOpen]);
const handleClose = useCallback(() => {
closeModal(modalId);
}, [modalId, closeModal]);
const handleUpdate = useCallback((updates: Partial<ModalState>) => {
updateModal(modalId, updates);
}, [modalId, updateModal]);
return {
// State
isOpen: modalState?.isOpen || false,
isAnimating: modalState?.isAnimating || false,
zIndex: modalState?.zIndex || 1000,
options: modalState?.options || {},
// Actions
open: handleOpen,
close: handleClose,
update: handleUpdate,
// Utility
isActive: activeModalId === modalId,
backdropCount: backdropStack.size
};
};
// Modal context provider for global state access
export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="modal-provider">
{children}
<div id="modal-root" />
</div>
);
};
// Example usage in component
const ExampleModalComponent: React.FC = () => {
const { isOpen, open, close } = useModal('example-modal');
return (
<div>
<button onClick={open}>Open Modal</button>
{isOpen && (
<Modal id="example-modal">
<div>Modal Content</div>
</Modal>
)}
</div>
);
};
The hook returns:
isOpen: Boolean indicating modal visibilityisAnimating: Animation state for transitionszIndex: Stacked positioning valueoptions: Configuration optionsopen,close,update: Action handlersisActive: Whether this modal is currently activebackdropCount: Number of modals in the backdrop stack
Key state transitions:
open→ setsisOpen: true, triggers animationclose→ setsisOpen: false, removes from stackupdate→ partial state updates for options/content changes
Side-effect handlers:
- Keyboard event listeners for escape key handling
- Backdrop stacking management via useEffect
- Animation state transitions with timeout handlers
4. Routing & Navigation
Route structure and hierarchy
For a modal/dialog feature, routing is typically not the primary concern since modals are usually rendered as overlays within the current route context. However, if we're building a feature that supports deep linking to modal states:
graph TD
A[Root Route] --> B[Dashboard]
A --> C[User Profile]
A --> D[Settings]
B --> E[Dashboard with Modal]
C --> F[User Profile Modal]
D --> G[Settings Modal]
In a typical application, modals are rendered as part of the current route context rather than separate routes. The modal component would be conditionally rendered based on application state or URL parameters.
Code splitting strategy
Route-based code splitting: Modals would not typically be split by route since they're part of existing page flows. Instead, we'd use component-based code splitting where modal content is dynamically imported when the modal opens.
// Example of component-based code splitting for modal content
const ModalContent = React.lazy(() => import('./components/ModalContent'));
// In parent component:
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState<JSX.Element | null>(null);
const openModal = async () => {
setIsModalOpen(true);
const content = await import('./components/ModalContent');
setModalContent(<content.default />);
};
Navigation guards and authentication flows
Since modals are typically context-aware rather than route-based, navigation guards would be handled at the application level:
- Authentication: If a modal requires authenticated access, we'd show an authentication modal or redirect to login
- Authorization: Check if user has required permissions before opening the modal
- Navigation blocking: Prevent navigation while modal is open if it contains unsaved changes
Deep linking support and URL parameter design
For deep linking support, we'd use URL search parameters:
// URL structure examples:
// https://app.com/dashboard?modal=profile&id=123
// https://app.com/settings?modal=confirmation&message=delete
const useModalFromUrl = () => {
const [modalType, setModalType] = useState<string | null>(null);
const [modalParams, setModalParams] = useState<Record<string, string>>({});
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const modal = searchParams.get('modal');
const params: Record<string, string> = {};
for (const [key, value] of searchParams.entries()) {
if (key !== 'modal') params[key] = value;
}
if (modal) {
setModalType(modal);
setModalParams(params);
}
}, []);
return { modalType, modalParams };
};
Browser history management and scroll restoration
History management: Modals should not create new history entries when opened. We'll use history.replaceState() to update URL parameters without creating new entries:
const openModal = (modalType: string, params: Record<string, string> = {}) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('modal', modalType);
Object.entries(params).forEach(([key, value]) => {
searchParams.set(key, value);
});
window.history.replaceState({}, '', `?${searchParams.toString()}`);
setIsModalOpen(true);
};
const closeModal = () => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('modal');
window.history.replaceState({}, '', `?${searchParams.toString()}`);
setIsModalOpen(false);
};
Scroll restoration: When modals open, we need to prevent body scrolling and restore scroll position when closed:
useEffect(() => {
const originalScrollY = window.scrollY;
if (isModalOpen) {
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${window.innerWidth - document.documentElement.clientWidth}px`;
} else {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
window.scrollTo(0, originalScrollY);
}
return () => {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}, [isModalOpen]);
Route-level considerations
For applications that do support route-based modals, we'd implement:
- Layout routes: Create layout components that can conditionally render modals
- Nested route support: Allow modal rendering within nested route structures
- Route parameter handling: Properly parse and validate modal-specific route parameters
// Route configuration example for nested modals:
const routes = [
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{
path: 'users/:userId',
element: <UserDetail />,
modal: true
},
{
path: 'settings',
element: <Settings />,
modal: true
}
]
}
];
The key design decision is that modals should not fundamentally alter the routing structure but rather enhance existing navigation flows with overlay capabilities.
5. API Integration Layer
API client design pattern
We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent API interaction across the application. The client will handle authentication, request/response transformation, and error handling.
// api/client.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding auth token
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for handling errors
apiClient.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
// Handle unauthorized access - redirect to login or clear auth
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
Request/response interceptors
Authentication token injection is handled via the request interceptor, while response interception handles authentication failures and normalizes error responses. The client will also handle retry logic for transient network errors.
Error handling and retry strategy
For API calls within modals, we'll implement a retry mechanism with exponential backoff for network errors (502, 503, 504) and client timeouts. For user-initiated actions within modals (e.g., form submission), we'll show inline error messages rather than retrying automatically.
Loading, error, and empty states design
Modals will show loading indicators during data fetching with skeleton loaders or progress spinners. Error states will display user-friendly messages with retry buttons or fallback options. Empty states will show appropriate messaging based on the modal's purpose.
Data fetching strategy
For modals that require data, we'll use React Query for server state management. Modals will fetch data on mount or when triggered, with caching and automatic refetching based on query configuration.
API response transformation layer
We'll implement a DTO-to-domain model mapping layer to normalize API responses:
// types/api.ts
export interface UserDTO {
id: string;
name: string;
email: string;
avatarUrl?: string;
}
export interface User {
id: string;
name: string;
email: string;
avatarUrl?: string;
}
export const mapUserDTOToDomain = (dto: UserDTO): User => ({
id: dto.id,
name: dto.name,
email: dto.email,
avatarUrl: dto.avatarUrl,
});
Sequence diagram for primary data flows
sequenceDiagram
participant M as Modal Component
participant Q as React Query
participant A as API Client
participant S as Server
M->>Q: Fetch data with query key
Q->>A: Make API request
A->>S: HTTP GET /api/users/123
S-->>A: Response (200)
A-->>Q: Transformed data
Q-->>M: Update UI with data
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for modal content loading
- Interaction to Next Paint (INP): <200ms for modal interactions
- Cumulative Layout Shift (CLS): <0.1 for modal content and backdrop
Bundle Size Budget per Route
- Modal component bundle: <5KB (minified + gzipped)
- Modal backdrop and animation utilities: <1KB
- Accessibility and keyboard navigation helpers: <2KB
Critical Rendering Path Optimization
- Modal component must be rendered in critical rendering path to avoid FOUC
- Use CSS
will-changeproperty for animation-ready elements - Preload modal content when pre-rendered or cached (e.g., via React Suspense)
- Defer non-critical modal content rendering until modal is opened
Image Optimization Strategy
- All images within modals use
loading="lazy"attribute - Support for WebP format with fallbacks to JPEG/PNG
- Responsive image handling via
srcSetandsizesattributes - Image preloading for frequently accessed modal content (e.g., profile avatars)
Font Loading Strategy
- Critical font subset loaded via
font-display: swapCSS property - Non-critical font variants loaded asynchronously with
font-display: fallback - Web font preloading using
<link rel="preload">for modal content fonts - Fallback to system fonts during font loading delays
Memoization Strategy
// Key memoization decisions for modal component
const Modal = React.memo(({ isOpen, title, children, onClose }: ModalProps) => {
// Memoize computed styles and layout calculations
const modalStyles = useMemo(() => ({
transform: isOpen ? 'translateY(0)' : 'translateY(100%)',
opacity: isOpen ? 1 : 0,
}), [isOpen]);
// Memoize event handlers to prevent re-renders
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) onClose();
}, [onClose]);
// Avoid re-computing static content
const modalContent = useMemo(() => (
<div className="modal-content">
{children}
</div>
), [children]);
return (
<div className="modal-backdrop" style={modalStyles} onClick={handleBackdropClick}>
{modalContent}
</div>
);
});
Virtualization for Large Lists
For modals containing large lists:
- Use
react-windoworreact-virtualfor virtualized rendering - Implement scroll position preservation when modal is reopened
- Use
shouldResetAfterStretchto handle dynamic content size changes
Prefetching and Preloading Strategy
- Preload modal templates or frequently used content (e.g., user profile modals)
- Use
IntersectionObserverto prefetch modal content when near viewport - Implement preloading of images and assets for modal content that's likely to be viewed soon
- Use
rel="prefetch"orrel="preload"for critical modal assets (e.g., icons, avatars)
// Example prefetching strategy for modal content
const useModalPrefetch = (modalId: string) => {
useEffect(() => {
const prefetchContent = async () => {
// Prefetch modal content when component mounts or modalId changes
const content = await fetchModalContent(modalId);
// Store in cache or preload resources
cache.set(`modal-${modalId}`, content);
};
prefetchContent();
}, [modalId]);
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. For modal components, we'll implement error boundaries at the modal root level and potentially at content level for complex nested components.
graph TD
A[App] --> B[Modal]
B --> C[ModalHeader]
B --> D[ModalBody]
B --> E[ModalFooter]
D --> F[ComplexContent]
F --> G[ErrorBoundary]
Global Error Handler Design
Implement a global error handler that catches unhandled errors and displays user-friendly feedback:
// ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Modal error:', error, errorInfo);
// Log to monitoring service
}
render() {
if (this.state.hasError) {
return (
<div className="modal-error">
<h2>Something went wrong</h2>
<p>We're sorry, but we couldn't display this modal.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
Offline / Network Failure Handling
For modals that depend on external data, implement graceful degradation:
// ModalWithFallback.tsx
import React, { useState, useEffect } from 'react';
interface ModalProps {
title: string;
isOpen: boolean;
onClose: () => void;
data?: any;
}
const ModalWithFallback: React.FC<ModalProps> = ({
title,
isOpen,
onClose,
data
}) => {
const [isOnline, setIsOnline] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const handleRetry = () => {
setIsLoading(true);
setError(null);
// Retry logic here
};
if (!isOnline) {
return (
<div className="modal offline-modal">
<div className="modal-content">
<h2>Offline Mode</h2>
<p>You're currently offline. Some features may not be available.</p>
<button onClick={handleRetry}>Try Again</button>
</div>
</div>
);
}
if (error) {
return (
<div className="modal error-modal">
<div className="modal-content">
<h2>Load Error</h2>
<p>{error}</p>
<button onClick={handleRetry}>Retry</button>
</div>
</div>
);
}
return (
<div className="modal">
<div className="modal-content">
<h2>{title}</h2>
{data ? <div>{data}</div> : <p>Loading...</p>}
</div>
</div>
);
};
Graceful Degradation Patterns
When modals fail to load or render properly, fall back to minimal functionality:
- Static fallback: Show basic modal with error message instead of complex content
- Progressive enhancement: Load minimal content first, then enhance with additional data
- Read-only mode: Display data in read-only format when edit functionality fails
User Feedback on Errors
Implement multiple error feedback mechanisms:
// ToastError.tsx
import React from 'react';
interface ToastProps {
message: string;
type: 'error' | 'warning' | 'info';
onClose: () => void;
}
const ToastError: React.FC<ToastProps> = ({ message, type, onClose }) => {
return (
<div className={`toast toast-${type}`}>
<span>{message}</span>
<button onClick={onClose}>×</button>
</div>
);
};
// ErrorContext.tsx
import React, { createContext, useContext, useState } from 'react';
interface ErrorContextType {
addError: (message: string, type?: 'error' | 'warning' | 'info') => void;
removeError: (id: string) => void;
}
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);
export const ErrorProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const [errors, setErrors] = useState<{ id: string; message: string; type: string }[]>([]);
const addError = (message: string, type: 'error' | 'warning' | 'info' = 'error') => {
const id = Date.now().toString();
setErrors(prev => [...prev, { id, message, type }]);
// Auto-remove after timeout
setTimeout(() => removeError(id), 5000);
};
const removeError = (id: string) => {
setErrors(prev => prev.filter(error => error.id !== id));
};
return (
<ErrorContext.Provider value={{ addError, removeError }}>
{children}
<div className="toast-container">
{errors.map(error => (
<ToastError
key={error.id}
message={error.message}
type={error.type as any}
onClose={() => removeError(error.id)}
/>
))}
</div>
</ErrorContext.Provider>
);
};
export const useError = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useError must be used within ErrorProvider');
}
return context;
};
Error Recovery Strategy
Implement a structured recovery approach:
- Immediate retry: For transient network errors, attempt retry immediately
- Exponential backoff: For persistent errors, implement increasing delays between retries
- User-controlled recovery: Provide explicit retry buttons for user-initiated actions
Error Logging and Monitoring
// errorLogger.ts
interface ErrorLog {
timestamp: Date;
message: string;
stack?: string;
componentStack?: string;
userAgent: string;
url: string;
}
const logError = (error: Error, componentStack?: string) => {
const errorLog: ErrorLog = {
timestamp: new Date(),
message: error.message,
stack: error.stack,
componentStack,
userAgent: navigator.userAgent,
url: window.location.href
};
// Send to monitoring service (Sentry, etc.)
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { componentStack }
});
}
console.error('Modal Error:', errorLog);
};
Resilience Patterns
- Timeout handling: Set timeouts for data fetching to prevent hanging modals
- Cache fallbacks: Use cached data when network requests fail
- Progressive loading: Show skeleton loaders while content loads
- Graceful state transitions: Ensure modal states don't break when errors occur
Error Recovery Flow
stateDiagram-v2
[*] --> Idle
Idle --> Loading : Open Modal
Loading --> Success : Data Loaded
Loading --> Error : Timeout/Network Error
Error --> Retry : User Clicks Retry
Retry --> Loading : Retry Attempt
Retry --> Failed : Max Retries Reached
Failed --> Idle : User Closes Modal
The error handling strategy prioritizes user experience by providing clear feedback, graceful degradation, and recovery mechanisms while maintaining system stability through proper error logging and monitoring integration.
8. Testing Strategy
Unit Testing Approach
Component-level testing (70% of test coverage):
- Test modal rendering with different props combinations
- Validate close button behavior and keyboard interactions
- Verify backdrop click and escape key handling
- Test modal content rendering with various children
- Validate focus trap behavior in different DOM states
Utility function testing (20% of test coverage):
- Test focus management helper functions
- Validate keyboard event handlers
- Test DOM element query utilities
State management testing (10% of test coverage):
- Test modal state transitions and side effects
- Validate hook return values and error handling
Test Coverage Targets:
- Components: 90% line coverage, 85% branch coverage
- Utilities: 100% line coverage
- State logic: 95% line coverage
Integration Testing Strategy
Modal composition testing (20% of integration tests):
- Test modal with form elements and validation
- Validate nested modal interactions
- Test modal lifecycle with parent component state changes
Accessibility integration (30% of integration tests):
- End-to-end ARIA attribute validation
- Keyboard navigation flow testing
- Screen reader compatibility verification
User interaction flows (50% of integration tests):
- Multi-step modal workflows
- Modal stacking and z-index management
- Responsive behavior across device sizes
E2E Testing Scope
Primary flows covered with Playwright:
- Basic modal open/close functionality
- Keyboard navigation accessibility
- Focus trapping and tab sequence validation
- Modal content interaction patterns
Secondary flows (limited coverage):
- Modal with external API calls
- Modal in different routing contexts
- Modal performance under load conditions
Visual Regression Testing
Test setup:
- 3 viewport sizes (mobile, tablet, desktop)
- 5 major browser contexts (Chrome, Firefox, Safari, Edge, Mobile Safari)
- Dark/light mode variants for all test cases
Test coverage:
- Default modal appearance
- Custom size and position variants
- Modal with different content types (text, forms, lists)
- Error states and validation messages
Accessibility Testing Automation
Axe-core integration:
- Run accessibility checks on all modal variants
- Validate ARIA role and property combinations
- Test screen reader announcement patterns
Keyboard navigation testing:
- Tab sequence validation across modal content
- Focus restoration after modal close
- Keyboard-only interaction support
Test Data Strategy
Test fixtures:
- Minimal content templates for different modal types (alert, form, confirmation)
- Edge case scenarios (empty content, very long content, complex forms)
- Internationalized content variants for i18n testing
Test data generation:
- Dynamic content creation for performance testing
- Mock API responses for form submission scenarios
- Error response patterns for error state validation
Test data isolation:
- Use unique identifiers for each test case
- Ensure no cross-contamination between test runs
- Mock external dependencies consistently across tests
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The modal component must meet the following WCAG 2.1 AA requirements:
- 1.1.1 Non-text Content: All non-text content must have text alternatives (e.g., aria-labels on close buttons)
- 1.3.1 Info and Relationships: Content structure must be semantically correct (landmark roles, proper heading hierarchy)
- 1.4.1 Use of Color: Color must not be the sole method for conveying information
- 1.4.3 Contrast (Minimum): Text contrast ratio must be at least 4.5:1 for normal text and 3:1 for large text
- 1.4.12 Text Spacing: Text must be able to be resized without loss of content or functionality
- 2.1.1 Keyboard: All functionality must be operable via keyboard alone (Tab navigation, Enter/Space activation)
- 2.1.2 No Keyboard Trap: Users must be able to navigate away from the modal using keyboard only
- 2.4.1 Bypass Blocks: Users must be able to skip past the modal content (skip links)
- 2.4.3 Focus Order: Tab order must follow logical reading order and return to the trigger element
- 2.4.6 Headings and Labels: All content must have descriptive headings or labels
- 3.1.1 Language: Content language must be programmatically determinable
- 4.1.2 Name, Role, Value: All interactive elements must have proper ARIA attributes
Keyboard navigation support
The modal must support full keyboard navigation:
-
Tab Navigation:
- Focus cycles through all interactive elements within the modal
- Focus returns to the first element when Tab is pressed at the end
- Focus moves to the last element when Shift+Tab is pressed at the beginning
-
Escape Key:
- Pressing Escape key closes the modal and returns focus to the trigger element
-
Enter/Space Activation:
- Buttons and interactive elements can be activated using Enter or Space keys
Focus management strategy
Focus trapping:
- The modal must trap focus within its boundaries using
focus-trapor similar library - When the modal opens, focus moves to the first interactive element (or the modal itself if no interactive elements exist)
- Focus should cycle between all focusable elements within the modal
Focus restoration:
- When the modal closes, focus must return to the element that opened it
- Focus restoration should happen after the modal is fully closed (after animation completes)
Focus management implementation approach:
// Focus trap implementation example
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLElement>) => {
useEffect(() => {
if (!isOpen) return;
const modal = modalRef.current;
if (!modal) return;
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (document.activeElement === lastElement && !e.shiftKey) {
e.preventDefault();
firstElement.focus();
} else if (document.activeElement === firstElement && e.shiftKey) {
e.preventDefault();
lastElement.focus();
}
}
};
modal.addEventListener('keydown', handleTabKey);
return () => {
modal.removeEventListener('keydown', handleTabKey);
};
}, [isOpen]);
};
ARIA roles and attributes plan
Core ARIA attributes:
role="dialog"on the modal container elementaria-modal="true"to indicate it's a modal dialogaria-labelledbyreferencing the modal title element (if present)aria-describedbyreferencing the modal content area (if present)aria-hidden="true"on all elements outside the modal when it's open
Focus management ARIA attributes:
tabindex="-1"on the modal container to ensure it can receive focusaria-labelon close buttons for screen reader users
Screen reader compatibility
Screen reader testing considerations:
- NVDA, JAWS, VoiceOver: All major screen readers must announce the modal content properly
- Announcement order: Screen reader should announce the modal title first, then content, then available actions
- Landmark roles: Use
role="region"witharia-labelto help screen reader users identify the modal as a distinct section - Dynamic content updates: When content changes dynamically, ensure screen readers are notified
ARIA live region implementation:
// For dynamic content updates within modal
const LiveRegion = ({ message }: { message: string }) => (
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
);
Screen reader user experience:
- When modal opens, screen readers should announce "Dialog opened"
- When modal closes, screen readers should announce "Dialog closed"
- All interactive elements within the modal must be announced with their purpose
- The modal should not interrupt ongoing screen reader reading of content outside the modal
Keyboard navigation hints for screen readers:
- Include
aria-describedbythat explains keyboard navigation (e.g., "Press Escape to close") - Use
role="status"for status updates that should be announced immediately
Accessibility testing automation
axe-core integration:
// Example of accessibility testing in component tests
import { axe, toHaveNoViolations } from 'axe-core';
expect.extend(toHaveNoViolations);
it('should pass accessibility tests', async () => {
const { container } = render(<Modal isOpen={true} title="Test Modal">Content</Modal>);
const results = await axe.run(container);
expect(results).toHaveNoViolations();
});
Keyboard navigation testing:
- Test that all interactive elements can be reached via keyboard
- Verify that focus order matches visual reading order
- Confirm that Escape key properly closes the modal and returns focus
Screen reader testing strategy:
- Use automated tools like axe to check for ARIA compliance
- Manual testing with NVDA, JAWS, and VoiceOver on supported platforms
- Test that modal content is properly announced when opened
10. Security Considerations
XSS Prevention Strategy
The modal component must implement robust XSS prevention to safeguard against malicious content injection:
-
Content Sanitization: All dynamic content passed to modal props (title, body, buttons) must be sanitized using a library like DOMPurify before rendering to prevent script injection.
-
HTML Escaping: Any user-provided content should be HTML-escaped by default to prevent direct DOM manipulation.
-
Prop Validation: All props accepting content should be strictly validated using PropTypes or TypeScript to ensure they're of expected types.
// Example of XSS prevention in modal component
const Modal = ({
title,
children,
actions,
isOpen
}: ModalProps) => {
const sanitizedTitle = DOMPurify.sanitize(title);
const sanitizedChildren = DOMPurify.sanitize(children);
return (
<div className="modal-overlay" aria-hidden={!isOpen}>
<div className="modal-content">
<h2 dangerouslySetInnerHTML={{ __html: sanitizedTitle }} />
<div dangerouslySetInnerHTML={{ __html: sanitizedChildren }} />
</div>
</div>
);
};
CSRF Protection
When modals trigger form submissions or API calls:
-
Token Inclusion: All modal forms must include CSRF tokens in their payload to prevent cross-site request forgery.
-
Secure Token Handling: Tokens should never be exposed in client-side logs or console output.
-
Token Rotation: Implement token rotation strategies for sensitive operations within modals.
Content Security Policy (CSP) Headers
The modal component should be compatible with CSP policies:
-
Inline Script Restrictions: Avoid inline JavaScript in modal templates and use event delegation instead.
-
External Resource Loading: All external resources (images, scripts) loaded within modals must comply with CSP directives.
-
Frame Ancestors: Ensure modal containers don't violate frame ancestor policies that might block embedding.
Sensitive Data Handling in Frontend State
-
State Isolation: Modal content should not persist sensitive data in global state or localStorage.
-
Memory Cleanup: Ensure sensitive data is properly cleaned from memory when modals are closed.
-
Secure Prop Passing: Avoid passing sensitive data through modal props that might be logged or cached.
Input Sanitization Approach
-
Form Data Validation: Any form inputs within modals should undergo server-side validation even when client-side sanitization occurs.
-
Input Length Limits: Implement strict limits on input lengths to prevent buffer overflow attacks.
-
Type Validation: Validate all input types (string, number) before processing within modal context.
// Example secure input handling in modal form
const SecureModalForm = () => {
const [formData, setFormData] = useState({
userInput: '',
sensitiveField: ''
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Sanitize input before setting state
const sanitizedValue = DOMPurify.sanitize(e.target.value);
setFormData(prev => ({
...prev,
[e.target.name]: sanitizedValue
}));
};
return (
<form onSubmit={handleSubmit}>
<input
name="userInput"
value={formData.userInput}
onChange={handleInputChange}
maxLength={255} // Prevent oversized inputs
/>
</form>
);
};
Security Testing Considerations
-
Vulnerability Scanning: Regular security scanning of modal components for known vulnerabilities.
-
Penetration Testing: Test modal interactions for potential security flaws in content rendering and state management.
-
Security Code Review: Include modal components in regular security code reviews to ensure no insecure practices are introduced.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys will follow a hierarchical, descriptive naming convention to ensure maintainability and clarity:
modal:
confirm:
title: "Confirm Action"
description: "Are you sure you want to proceed?"
confirmButton: "Confirm"
cancelButton: "Cancel"
error:
title: "Error Occurred"
description: "Something went wrong. Please try again."
Keys will be organized under feature-specific namespaces (e.g., modal, notification) to prevent naming collisions and support modular development.
RTL Support Considerations
RTL (Right-to-Left) language support will be handled through CSS class toggling based on detected locale direction:
// In a context or hook that manages RTL state
const isRTL = useIsRTL(); // returns boolean based on current locale
// Apply RTL class conditionally to modal container
<div className={`modal ${isRTL ? 'rtl' : ''}`}>
CSS will handle layout adjustments:
.modal.rtl {
direction: rtl;
text-align: right;
}
Date, Number, Currency Formatting Strategy
Date and number formatting will leverage the browser's built-in Intl API to ensure proper localization:
// Example usage in modal content
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(amount);
};
Locale Detection and Switching Strategy
Automatic locale detection will be implemented using the browser's navigator.language property:
// Detect user's preferred language
const detectLocale = (): string => {
const browserLang = navigator.language;
const supportedLocales = ['en', 'fr', 'de', 'es'];
return supportedLocales.includes(browserLang)
? browserLang
: 'en'; // fallback to English
};
// Store in context or global state for consistent usage across app
Locale switching will be handled via a dedicated hook that updates both the translation context and DOM direction:
// Hook for managing locale changes
const useLocaleSwitcher = () => {
const [currentLocale, setCurrentLocale] = useState<string>('en');
const switchLocale = (newLocale: string) => {
setCurrentLocale(newLocale);
document.dir = isRTL(newLocale) ? 'rtl' : 'ltr';
};
return { currentLocale, switchLocale };
};
The system will support dynamic loading of translation files using React's lazy loading patterns to minimize initial bundle size while allowing for efficient switching between languages.
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The modal component should capture key performance indicators for user experience:
- Modal Open Time: Time from
openevent to full render completion (target < 100ms) - Modal Close Time: Time from
closeevent to complete removal (target < 50ms) - Interaction Latency: Time from user click to modal state change (target < 100ms)
- Scroll Lock Performance: Time to lock/unlock document scroll (target < 50ms)
- Focus Trap Efficiency: Time to establish focus trap (target < 20ms)
graph LR
A[User Click] --> B[Modal Open Event]
B --> C[Render Modal]
C --> D[Scroll Lock]
D --> E[Focus Trap Setup]
E --> F[Ready State]
Error Tracking Integration
Implement Sentry or equivalent error tracking with:
- Modal Lifecycle Errors: Failed renders, focus loss, scroll lock failures
- Accessibility Violations: ARIA attribute mismatches, keyboard trap issues
- Performance Degradation: Slow open/close operations exceeding thresholds
Custom Events and User Journey Tracking
Track modal-specific user journeys:
// Event tracking for user behavior analysis
const trackModalEvent = (event: string, properties?: Record<string, any>) => {
// Send to analytics service
window.analytics?.track(`modal_${event}`, properties);
};
// Example usage in modal component
const handleOpen = () => {
trackModalEvent('opened', {
modalType: 'confirmation',
triggerSource: 'button_click'
});
};
Performance Monitoring and Regression Detection
- LCP (Largest Contentful Paint): Monitor modal content rendering
- FID (First Input Delay): Track responsiveness during modal interactions
- CLS (Cumulative Layout Shift): Ensure modal doesn't cause layout shifts
Implement performance monitoring hooks:
// Performance monitoring hook for modal interactions
const useModalPerformance = () => {
const [metrics, setMetrics] = useState({
openTime: 0,
closeTime: 0,
interactionLatency: 0
});
const measureOpenTime = (startTime: number) => {
const openDuration = performance.now() - startTime;
setMetrics(prev => ({ ...prev, openTime: openDuration }));
// Alert on performance degradation
if (openDuration > 150) {
console.warn('Modal open time exceeded threshold', openDuration);
}
};
return { metrics, measureOpenTime };
};
Alerting and Thresholds
- Critical: Modal open/close times > 200ms → alert team
- Warning: Modal interaction latency > 150ms → log performance issue
- Success: Normal operation within target thresholds
Data Collection Strategy
- Anonymous Usage Analytics: Track modal usage patterns without personal data
- Feature Adoption Metrics: Count of modals opened by type and user segment
- Failure Rate Monitoring: Percentage of failed modal operations
Integration with Existing Observability Stack
The component should integrate with existing monitoring infrastructure:
// Example integration pattern
const modalMonitoring = {
captureError: (error: Error, context: string) => {
// Send to error tracking service
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { modal: context }
});
}
},
captureMetric: (name: string, value: number) => {
// Send to performance monitoring service
if (window.performance) {
window.performance.mark(`${name}_${Date.now()}`);
}
}
};
This monitoring approach ensures the modal component maintains high performance, provides actionable insights into user behavior, and enables proactive issue detection before they impact users.
13. File & Folder Structure
Proposed Directory Structure
src/
├── components/
│ ├── Modal/
│ │ ├── Modal.tsx
│ │ ├── ModalHeader.tsx
│ │ ├── ModalBody.tsx
│ │ ├── ModalFooter.tsx
│ │ ├── useModal.ts
│ │ └── index.ts
│ ├── Button/
│ │ └── Button.tsx
│ └── index.ts
├── hooks/
│ └── useModal.ts
├── types/
│ └── modal.ts
└── utils/
└── modalUtils.ts
Module Boundaries and Import Rules
- Modal component is self-contained with no cross-feature dependencies
- All internal components (
ModalHeader,ModalBody, etc.) are exported through the mainModalbarrel export - Components in
src/components/can only import from:- Other components in the same directory
- Shared utilities and types
- Design system primitives (Button, Typography)
- No feature-specific components should import from other features directly
Shared vs Feature-Specific Code Separation
Shared Components:
Modaland its sub-components are shared across featuresButtoncomponent is reused throughout the application
Feature-Specific Code:
- Feature-specific modals would be created in their respective feature folders
- Example:
features/user-profile/Modals/UserProfileModal.tsx
The structure follows a feature-based organization pattern where each feature owns its modals, but the base Modal component is shared across the application. This provides clean separation of concerns while maintaining reusability for core modal functionality.
// src/components/Modal/index.ts
export { default as Modal } from './Modal';
export { default as ModalHeader } from './ModalHeader';
export { default as ModalBody } from './ModalBody';
export { default as ModalFooter } from './ModalFooter';
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
React (18.2+) - Core library for component-based UI development with concurrent rendering capabilities. Essential for modern React patterns and hooks ecosystem.
ReactDOM (18.2+) - Required for DOM rendering and React's ability to work with the browser DOM.
Styled Components (5.3+) - Provides CSS-in-JS solution with type safety and theme support. Offers better scoping than traditional CSS, improves maintainability, and enables dynamic styling based on component props.
Framer Motion (10.12+) - Production-ready animation library that provides performant, declarative animations with minimal configuration. Perfect for modal entrance/exit animations and smooth transitions.
React Portal (built-in) - Part of React core, enables rendering children outside the DOM hierarchy. Critical for modals to ensure proper z-index stacking and accessibility.
React Focus Trap (7.0+) - Ensures keyboard focus remains within modal boundaries, crucial for accessibility compliance and proper keyboard navigation.
React Hook Form (7.40+) - Comprehensive form validation and management solution that provides excellent performance, type safety, and reduces boilerplate code for forms within modals.
React Query (5.0+) - Modern data fetching solution that handles caching, background updates, and server state management. Essential for modal-based data operations.
Zod (3.20+) - TypeScript-first schema validation library that provides runtime type checking and error handling for form data within modals.
date-fns (2.30+) - Modern, lightweight date formatting library that's tree-shakeable and supports internationalization for date-related modal content.
Alternatives Considered and Rejected
Emotion - While powerful, Emotion's more complex configuration and slightly higher bundle size compared to Styled Components made it less attractive for this use case.
Tailwind CSS - Though popular, Tailwind's utility-first approach doesn't align well with the component-driven design philosophy and accessibility requirements of modal components.
React Bootstrap - Provides modal components but is too heavy for this specific use case and introduces unnecessary dependencies like jQuery and Bootstrap CSS.
Material UI - While comprehensive, MUI's modal implementation is too opinionated and would require significant customization to meet our specific accessibility and animation requirements.
Licensing and Bundle Size Impact Assessment
React + ReactDOM: MIT license, minimal bundle impact (~15KB minified) - Essential dependencies.
Styled Components: MIT license, moderate bundle impact (~30KB minified) - Provides significant value in maintainability and type safety.
Framer Motion: MIT license, small bundle impact (~20KB minified) - Critical for smooth animations and user experience.
React Focus Trap: MIT license, minimal impact (~5KB minified) - Essential for accessibility compliance.
React Hook Form: MIT license, minimal impact (~25KB minified) - Reduces form boilerplate and improves validation.
React Query: MIT license, moderate impact (~35KB minified) - Critical for data fetching and caching.
Zod: MIT license, minimal impact (~20KB minified) - Provides type safety and runtime validation.
date-fns: MIT license, minimal impact (~15KB minified) - Tree-shakeable and lightweight for date operations.
Bundle Size Budget Considerations
Total modal-related dependencies: ~140KB (minified + gzipped) - This is acceptable as it's a core UI component that will be used across the application. The bundle size is justified by the enhanced user experience, accessibility features, and type safety provided.
All dependencies are well-maintained with active development and strong TypeScript support, ensuring long-term maintainability and compatibility with modern React patterns.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Modal stacking behavior: Should modals stack visually or overlay? How many modals should be allowed to stack before blocking new ones?
- Escape key behavior: Should escape key close all modals or just the topmost one? What about nested modals?
- Backdrop click behavior: Should clicking backdrop close modal? What about nested modals with multiple backdrops?
- Focus trapping strategy: Should focus be trapped within the modal or allow focus to escape when appropriate?
Known trade-offs made in this design
- Simplified animation system: Prioritized clean, performant transitions over complex animation libraries. This trades flexibility for maintainability.
- Limited accessibility features: Implemented core ARIA patterns but didn't include full keyboard navigation support for complex modal content (e.g., nested forms) to keep scope manageable.
- Hardcoded z-index values: Used fixed z-index values instead of dynamic stacking context to avoid complexity but limit extensibility.
- Single portal target: Defaulted to
document.bodyinstead of allowing configurable portal targets to reduce API surface and potential DOM issues.
Risks and mitigations
- Focus trap edge cases: Risk of focus trapping breaking when content dynamically changes. Mitigation: Implement robust focus restoration logic and provide escape hatches for complex cases.
- Performance impact from portals: Risk of rendering overhead when many modals are open. Mitigation: Use React's
createPortalefficiently and provide clear lifecycle management. - Accessibility regression: Risk of breaking existing keyboard navigation or screen reader support. Mitigation: Comprehensive A11y testing and integration with existing accessibility tooling.
- Styling conflicts: Risk of modal styles clashing with host application's CSS. Mitigation: Scoped styling approach with CSS modules or CSS-in-JS, and clear documentation on customization.
Technical debt acknowledged
- Limited theming support: Current design doesn't support extensive theme customization without significant refactoring.
- No built-in animation controls: Animation timing and easing are hardcoded, requiring custom implementations for advanced use cases.
- Basic keyboard navigation: Only basic focus management implemented; complex nested content requires additional workarounds.
- Limited content composition: Modal body is currently restrictive in what content it can accept, requiring custom wrapper components for complex layouts.
- No automatic sizing: Modals don't auto-size based on content, requiring manual size management for optimal UX.