Skip to main content

Modal Dialog Component Design and Implementation

Comprehensive low-level design for accessible, customizable modal/dialog components with React and TypeScript

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

  1. What target browsers/devices are we supporting? (e.g., IE11, mobile Safari, Chrome 80+)
  2. Should this modal support real-time updates or is it primarily static content?
  3. Do we need to support nested modals or stacking behavior?
  4. Is there a specific design system or component library we must integrate with?
  5. Should this modal support keyboard navigation and screen reader accessibility out of the box?
  6. Do we need to support internationalization (i18n) with RTL layout support?
  7. 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 handling
  • ModalManager - Global modal registry and keyboard event handler (optional)

Dumb Components (Presentational):

  • ModalBackdrop - Semi-transparent overlay background
  • ModalContainer - Main modal container with positioning and animation
  • ModalHeader - Title and close button container
  • ModalBody - Content area with scroll handling
  • ModalFooter - Action buttons container
  • ModalCloseButton - 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

  1. Mount: Focus trap initialization, keyboard event listeners registration
  2. Update: State changes trigger animation transitions and accessibility updates
  3. 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:

  1. Clean separation of concerns - global state is isolated from component logic
  2. Minimal re-renders through proper selector usage
  3. Easy testing and mocking capabilities
  4. 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 isOpen changes and content requires fresh data
  • Use React Query's refetchOnMount or refetchOnWindowFocus for 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 visibility
  • isAnimating: Animation state for transitions
  • zIndex: Stacked positioning value
  • options: Configuration options
  • open, close, update: Action handlers
  • isActive: Whether this modal is currently active
  • backdropCount: Number of modals in the backdrop stack

Key state transitions:

  1. open → sets isOpen: true, triggers animation
  2. close → sets isOpen: false, removes from stack
  3. update → 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 />);
};

Since modals are typically context-aware rather than route-based, navigation guards would be handled at the application level:

  1. Authentication: If a modal requires authenticated access, we'd show an authentication modal or redirect to login
  2. Authorization: Check if user has required permissions before opening the modal
  3. 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:

  1. Layout routes: Create layout components that can conditionally render modals
  2. Nested route support: Allow modal rendering within nested route structures
  3. 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-change property 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 srcSet and sizes attributes
  • Image preloading for frequently accessed modal content (e.g., profile avatars)

Font Loading Strategy

  • Critical font subset loaded via font-display: swap CSS 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-window or react-virtual for virtualized rendering
  • Implement scroll position preservation when modal is reopened
  • Use shouldResetAfterStretch to handle dynamic content size changes

Prefetching and Preloading Strategy

  • Preload modal templates or frequently used content (e.g., user profile modals)
  • Use IntersectionObserver to 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" or rel="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:

  1. Static fallback: Show basic modal with error message instead of complex content
  2. Progressive enhancement: Load minimal content first, then enhance with additional data
  3. 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:

  1. Immediate retry: For transient network errors, attempt retry immediately
  2. Exponential backoff: For persistent errors, implement increasing delays between retries
  3. 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

  1. Timeout handling: Set timeouts for data fetching to prevent hanging modals
  2. Cache fallbacks: Use cached data when network requests fail
  3. Progressive loading: Show skeleton loaders while content loads
  4. 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:

  1. 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
  2. Escape Key:

    • Pressing Escape key closes the modal and returns focus to the trigger element
  3. 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-trap or 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 element
  • aria-modal="true" to indicate it's a modal dialog
  • aria-labelledby referencing the modal title element (if present)
  • aria-describedby referencing 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 focus
  • aria-label on close buttons for screen reader users

Screen reader compatibility

Screen reader testing considerations:

  1. NVDA, JAWS, VoiceOver: All major screen readers must announce the modal content properly
  2. Announcement order: Screen reader should announce the modal title first, then content, then available actions
  3. Landmark roles: Use role="region" with aria-label to help screen reader users identify the modal as a distinct section
  4. 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-describedby that 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:

  1. 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.

  2. HTML Escaping: Any user-provided content should be HTML-escaped by default to prevent direct DOM manipulation.

  3. 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:

  1. Token Inclusion: All modal forms must include CSRF tokens in their payload to prevent cross-site request forgery.

  2. Secure Token Handling: Tokens should never be exposed in client-side logs or console output.

  3. 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:

  1. Inline Script Restrictions: Avoid inline JavaScript in modal templates and use event delegation instead.

  2. External Resource Loading: All external resources (images, scripts) loaded within modals must comply with CSP directives.

  3. Frame Ancestors: Ensure modal containers don't violate frame ancestor policies that might block embedding.

Sensitive Data Handling in Frontend State

  1. State Isolation: Modal content should not persist sensitive data in global state or localStorage.

  2. Memory Cleanup: Ensure sensitive data is properly cleaned from memory when modals are closed.

  3. Secure Prop Passing: Avoid passing sensitive data through modal props that might be logged or cached.

Input Sanitization Approach

  1. Form Data Validation: Any form inputs within modals should undergo server-side validation even when client-side sanitization occurs.

  2. Input Length Limits: Implement strict limits on input lengths to prevent buffer overflow attacks.

  3. 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

  1. Vulnerability Scanning: Regular security scanning of modal components for known vulnerabilities.

  2. Penetration Testing: Test modal interactions for potential security flaws in content rendering and state management.

  3. 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 open event to full render completion (target < 100ms)
  • Modal Close Time: Time from close event 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 main Modal barrel 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:

  • Modal and its sub-components are shared across features
  • Button component 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.body instead 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 createPortal efficiently 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.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Breadcrumb Component Design and Implementation

    Comprehensive low-level design for a reusable breadcrumb navigation component with React and TypeScript, covering architecture, props, and performance considerations.

  • Frontend LLD

    Tooltip Component Design and Implementation

    Comprehensive low-level design for a reusable, accessible tooltip component with dynamic positioning and performance optimizations

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.