Frontend Machine Coding: Build a Dynamic Modal Component

Learn how to build a flexible modal component with smooth animations, accessibility features, and configurable behavior using React

Frontend Machine Coding: Build a Dynamic Modal Component

Problem Statement

Build a reusable modal component that can handle different types of content with smooth animations, accessibility features, and configurable behavior. This is a common UI pattern used in forms, confirmations, image galleries, and content overlays.

Requirements

Must-Have Features:

  • Modal that opens/closes with smooth animations
  • Backdrop/overlay that can be clicked to close
  • Escape key to close modal
  • Focus management (trap focus inside modal)
  • Accessible with proper ARIA attributes
  • Configurable content and size
  • Responsive design

Nice-to-Have Features:

  • Different modal types (centered, fullscreen, side panel)
  • Custom animations and transitions
  • Multiple modals support
  • Form handling within modals
  • Loading states and error handling
  • Theme customization

🧠 Understanding the Problem

What is a Modal Component?

A modal component is a UI pattern that:

  • Displays content in an overlay above the main page
  • Captures user attention and prevents interaction with background
  • Provides a focused context for specific actions
  • Can contain forms, confirmations, or rich content
  • Follows accessibility guidelines for screen readers and keyboard users

Key Concepts to Implement:

  1. Portal Rendering: Render modal outside main component tree
  2. Focus Management: Trap focus and restore on close
  3. Animation System: Smooth enter/exit transitions
  4. Accessibility: ARIA attributes and keyboard navigation
  5. Event Handling: Backdrop clicks, escape key, etc.

🏗️ Solution Architecture

Component Structure:

Modal/
├── ModalProvider (Context for state management)
├── ModalContainer (Main wrapper with backdrop)
├── ModalContent (Actual modal content)
├── ModalHeader (Title and close button)
├── ModalBody (Main content area)
└── ModalFooter (Action buttons)

Data Flow:

  1. Modal state managed through context or props
  2. Portal renders modal at document body level
  3. Focus management handles keyboard navigation
  4. Event listeners handle backdrop and escape key

Complete Solution

Step 1: Define Interfaces and Types

First, let's define the structure for our modal component:

// Modal configuration interfaces
export interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title?: string;
  children: React.ReactNode;
  size?: 'small' | 'medium' | 'large' | 'fullscreen';
  variant?: 'centered' | 'side-panel' | 'bottom-sheet';
  closeOnBackdrop?: boolean;
  closeOnEscape?: boolean;
  showCloseButton?: boolean;
  className?: string;
  backdropClassName?: string;
  contentClassName?: string;
}

export interface ModalContextType {
  isOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
  modalId: string;
}

export interface ModalConfig {
  id: string;
  isOpen: boolean;
  onClose: () => void;
  content: React.ReactNode;
  config: Partial<ModalProps>;
}

Step 2: Create the Modal Context

// ModalContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';

const ModalContext = createContext<ModalContextType | null>(null);

export const useModal = () => {
  const context = useContext(ModalContext);
  if (!context) {
    throw new Error('useModal must be used within ModalProvider');
  }
  return context;
};

interface ModalProviderProps {
  children: React.ReactNode;
}

export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [modalId] = useState(() => `modal-${Math.random().toString(36).substr(2, 9)}`);

  const openModal = useCallback(() => {
    setIsOpen(true);
  }, []);

  const closeModal = useCallback(() => {
    setIsOpen(false);
  }, []);

  return (
    <ModalContext.Provider
      value={{
        isOpen,
        openModal,
        closeModal,
        modalId
      }}
    >
      {children}
    </ModalContext.Provider>
  );
};

Step 3: Create the Modal Portal

// ModalPortal.tsx
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

interface ModalPortalProps {
  children: React.ReactNode;
  isOpen: boolean;
}

export const ModalPortal: React.FC<ModalPortalProps> = ({ children, isOpen }) => {
  const [container, setContainer] = useState<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      const modalContainer = document.createElement('div');
      modalContainer.setAttribute('data-modal-portal', 'true');
      document.body.appendChild(modalContainer);
      setContainer(modalContainer);

      // Prevent body scroll when modal is open
      document.body.style.overflow = 'hidden';

      return () => {
        document.body.removeChild(modalContainer);
        document.body.style.overflow = '';
      };
    }
  }, [isOpen]);

  if (!isOpen || !container) {
    return null;
  }

  return createPortal(children, container);
};

Step 4: Create the Modal Container

// ModalContainer.tsx
import React, { useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';

interface ModalContainerProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
  closeOnBackdrop?: boolean;
  closeOnEscape?: boolean;
  backdropClassName?: string;
  contentClassName?: string;
}

export const ModalContainer: React.FC<ModalContainerProps> = ({
  isOpen,
  onClose,
  children,
  closeOnBackdrop = true,
  closeOnEscape = true,
  backdropClassName = '',
  contentClassName = ''
}) => {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousActiveElement = useRef<HTMLElement | null>(null);

  // Handle escape key
  const handleKeyDown = useCallback((event: KeyboardEvent) => {
    if (event.key === 'Escape' && closeOnEscape) {
      onClose();
    }
  }, [onClose, closeOnEscape]);

  // Handle backdrop click
  const handleBackdropClick = useCallback((event: React.MouseEvent) => {
    if (event.target === event.currentTarget && closeOnBackdrop) {
      onClose();
    }
  }, [onClose, closeOnBackdrop]);

  // Focus management
  useEffect(() => {
    if (isOpen) {
      // Store the currently focused element
      previousActiveElement.current = document.activeElement as HTMLElement;
      
      // Focus the modal
      if (modalRef.current) {
        modalRef.current.focus();
      }

      // Add event listeners
      document.addEventListener('keydown', handleKeyDown);
      
      // Prevent body scroll
      document.body.style.overflow = 'hidden';

      return () => {
        document.removeEventListener('keydown', handleKeyDown);
        document.body.style.overflow = '';
        
        // Restore focus to the previous element
        if (previousActiveElement.current) {
          previousActiveElement.current.focus();
        }
      };
    }
  }, [isOpen, handleKeyDown]);

  if (!isOpen) {
    return null;
  }

  return createPortal(
    <div
      className={`modal-backdrop ${backdropClassName}`}
      onClick={handleBackdropClick}
      role="dialog"
      aria-modal="true"
    >
      <div
        ref={modalRef}
        className={`modal-container ${contentClassName}`}
        tabIndex={-1}
        role="dialog"
        aria-modal="true"
      >
        {children}
      </div>
    </div>,
    document.body
  );
};

Step 5: Create the Modal Content

// ModalContent.tsx
import React from 'react';
import { CloseIcon } from './Icons';

interface ModalContentProps {
  title?: string;
  children: React.ReactNode;
  onClose: () => void;
  size?: 'small' | 'medium' | 'large' | 'fullscreen';
  variant?: 'centered' | 'side-panel' | 'bottom-sheet';
  showCloseButton?: boolean;
  className?: string;
}

export const ModalContent: React.FC<ModalContentProps> = ({
  title,
  children,
  onClose,
  size = 'medium',
  variant = 'centered',
  showCloseButton = true,
  className = ''
}) => {
  const getSizeClasses = () => {
    switch (size) {
      case 'small':
        return 'max-w-sm';
      case 'large':
        return 'max-w-4xl';
      case 'fullscreen':
        return 'w-full h-full max-w-none';
      default:
        return 'max-w-2xl';
    }
  };

  const getVariantClasses = () => {
    switch (variant) {
      case 'side-panel':
        return 'fixed right-0 top-0 h-full transform translate-x-0';
      case 'bottom-sheet':
        return 'fixed bottom-0 left-0 right-0 transform translate-y-0';
      default:
        return 'relative mx-auto my-8';
    }
  };

  return (
    <div
      className={`modal-content ${getSizeClasses()} ${getVariantClasses()} ${className}`}
      role="dialog"
      aria-labelledby={title ? 'modal-title' : undefined}
      aria-describedby="modal-description"
    >
      {(title || showCloseButton) && (
        <div className="modal-header">
          {title && (
            <h2 id="modal-title" className="modal-title">
              {title}
            </h2>
          )}
          {showCloseButton && (
            <button
              type="button"
              className="modal-close-button"
              onClick={onClose}
              aria-label="Close modal"
            >
              <CloseIcon />
            </button>
          )}
        </div>
      )}
      
      <div id="modal-description" className="modal-body">
        {children}
      </div>
    </div>
  );
};

Step 6: Create the Icons Component

// Icons.tsx
import React from 'react';

interface IconProps {
  className?: string;
}

export const CloseIcon: React.FC<IconProps> = ({ className = '' }) => (
  <svg
    className={className}
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      d="M18 6L6 18M6 6L18 18"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

Step 7: Create the Main Modal Component

// Modal.tsx
import React from 'react';
import { ModalContainer } from './ModalContainer';
import { ModalContent } from './ModalContent';

const Modal: React.FC<ModalProps> = ({
  isOpen,
  onClose,
  title,
  children,
  size = 'medium',
  variant = 'centered',
  closeOnBackdrop = true,
  closeOnEscape = true,
  showCloseButton = true,
  className = '',
  backdropClassName = '',
  contentClassName = ''
}) => {
  return (
    <ModalContainer
      isOpen={isOpen}
      onClose={onClose}
      closeOnBackdrop={closeOnBackdrop}
      closeOnEscape={closeOnEscape}
      backdropClassName={backdropClassName}
      contentClassName={contentClassName}
    >
      <ModalContent
        title={title}
        onClose={onClose}
        size={size}
        variant={variant}
        showCloseButton={showCloseButton}
        className={className}
      >
        {children}
      </ModalContent>
    </ModalContainer>
  );
};

export default Modal;

Step 8: Create the CSS Styles

/* Modal.css */
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  animation: fadeIn 0.3s ease-out;
}

.modal-container {
  background: white;
  border-radius: 8px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  max-height: 90vh;
  overflow-y: auto;
  animation: slideIn 0.3s ease-out;
}

.modal-content {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.5rem 1.5rem 0 1.5rem;
  border-bottom: 1px solid #e5e7eb;
  margin-bottom: 1rem;
}

.modal-title {
  font-size: 1.25rem;
  font-weight: 600;
  color: #1f2937;
  margin: 0;
}

.modal-close-button {
  background: none;
  border: none;
  padding: 0.5rem;
  cursor: pointer;
  border-radius: 4px;
  color: #6b7280;
  transition: all 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-close-button:hover {
  background-color: #f3f4f6;
  color: #374151;
}

.modal-body {
  padding: 0 1.5rem 1.5rem 1.5rem;
  flex: 1;
}

/* Animations */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(-20px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Variant styles */
.modal-content.side-panel {
  animation: slideInRight 0.3s ease-out;
}

@keyframes slideInRight {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

.modal-content.bottom-sheet {
  animation: slideInUp 0.3s ease-out;
}

@keyframes slideInUp {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

/* Responsive design */
@media (max-width: 768px) {
  .modal-container {
    margin: 1rem;
    max-height: calc(100vh - 2rem);
  }
  
  .modal-header {
    padding: 1rem 1rem 0 1rem;
  }
  
  .modal-body {
    padding: 0 1rem 1rem 1rem;
  }
}

/* Dark theme */
.modal-backdrop.dark {
  background-color: rgba(0, 0, 0, 0.7);
}

.modal-container.dark {
  background: #1f2937;
  color: #f9fafb;
}

.modal-container.dark .modal-title {
  color: #f9fafb;
}

.modal-container.dark .modal-close-button {
  color: #9ca3af;
}

.modal-container.dark .modal-close-button:hover {
  background-color: #374151;
  color: #f9fafb;
}

Step 9: Create Usage Examples

// App.tsx
import React, { useState } from 'react';
import Modal from './Modal';

const App: React.FC = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
  const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);

  return (
    <div className="app">
      <h1>Modal Examples</h1>
      
      <div className="button-group">
        <button 
          onClick={() => setIsModalOpen(true)}
          className="btn btn-primary"
        >
          Open Basic Modal
        </button>
        
        <button 
          onClick={() => setIsConfirmModalOpen(true)}
          className="btn btn-secondary"
        >
          Open Confirmation Modal
        </button>
        
        <button 
          onClick={() => setIsSidePanelOpen(true)}
          className="btn btn-tertiary"
        >
          Open Side Panel
        </button>
      </div>

      {/* Basic Modal */}
      <Modal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        title="Welcome to Our Platform"
        size="medium"
      >
        <div className="modal-content-example">
          <p>This is a basic modal with some content. You can put any React components here.</p>
          <div className="form-group">
            <label htmlFor="email">Email:</label>
            <input type="email" id="email" placeholder="Enter your email" />
          </div>
          <div className="form-group">
            <label htmlFor="message">Message:</label>
            <textarea id="message" placeholder="Enter your message" rows={4}></textarea>
          </div>
          <div className="button-group">
            <button className="btn btn-primary">Submit</button>
            <button 
              className="btn btn-secondary"
              onClick={() => setIsModalOpen(false)}
            >
              Cancel
            </button>
          </div>
        </div>
      </Modal>

      {/* Confirmation Modal */}
      <Modal
        isOpen={isConfirmModalOpen}
        onClose={() => setIsConfirmModalOpen(false)}
        title="Confirm Action"
        size="small"
      >
        <div className="confirmation-content">
          <p>Are you sure you want to perform this action? This cannot be undone.</p>
          <div className="button-group">
            <button 
              className="btn btn-danger"
              onClick={() => {
                console.log('Action confirmed');
                setIsConfirmModalOpen(false);
              }}
            >
              Confirm
            </button>
            <button 
              className="btn btn-secondary"
              onClick={() => setIsConfirmModalOpen(false)}
            >
              Cancel
            </button>
          </div>
        </div>
      </Modal>

      {/* Side Panel Modal */}
      <Modal
        isOpen={isSidePanelOpen}
        onClose={() => setIsSidePanelOpen(false)}
        title="Settings Panel"
        variant="side-panel"
        size="large"
      >
        <div className="settings-content">
          <div className="setting-group">
            <h3>General Settings</h3>
            <div className="setting-item">
              <label>
                <input type="checkbox" /> Enable notifications
              </label>
            </div>
            <div className="setting-item">
              <label>
                <input type="checkbox" /> Dark mode
              </label>
            </div>
          </div>
          
          <div className="setting-group">
            <h3>Privacy Settings</h3>
            <div className="setting-item">
              <label>
                <input type="checkbox" /> Share analytics data
              </label>
            </div>
          </div>
          
          <div className="button-group">
            <button className="btn btn-primary">Save Settings</button>
            <button 
              className="btn btn-secondary"
              onClick={() => setIsSidePanelOpen(false)}
            >
              Cancel
            </button>
          </div>
        </div>
      </Modal>
    </div>
  );
};

export default App;

Performance Optimizations

1. Memoization

const Modal = React.memo<ModalProps>(({ isOpen, onClose, children, ...props }) => {
  // Component implementation
});

2. Lazy Loading

const LazyModalContent = React.lazy(() => import('./ModalContent'));

// Use in Modal
{isOpen && (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyModalContent>{children}</LazyModalContent>
  </Suspense>
)}

3. Event Cleanup

useEffect(() => {
  if (isOpen) {
    const handleEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        onClose();
      }
    };
    
    document.addEventListener('keydown', handleEscape);
    
    return () => {
      document.removeEventListener('keydown', handleEscape);
    };
  }
}, [isOpen, onClose]);

Advanced Features

1. Custom Animations

// Custom animation hook
const useModalAnimation = (isOpen: boolean) => {
  const [isAnimating, setIsAnimating] = useState(false);
  const [shouldRender, setShouldRender] = useState(false);

  useEffect(() => {
    if (isOpen) {
      setShouldRender(true);
      setIsAnimating(true);
    } else {
      setIsAnimating(false);
      const timer = setTimeout(() => {
        setShouldRender(false);
      }, 300); // Match animation duration
      return () => clearTimeout(timer);
    }
  }, [isOpen]);

  return { shouldRender, isAnimating };
};

2. Multiple Modals Support

// Modal manager for multiple modals
const useModalManager = () => {
  const [modals, setModals] = useState<ModalConfig[]>([]);

  const openModal = useCallback((config: Omit<ModalConfig, 'isOpen'>) => {
    setModals(prev => [...prev, { ...config, isOpen: true }]);
  }, []);

  const closeModal = useCallback((id: string) => {
    setModals(prev => prev.map(modal => 
      modal.id === id ? { ...modal, isOpen: false } : modal
    ));
  }, []);

  return { modals, openModal, closeModal };
};

3. Form Handling

// Modal with form handling
const FormModal: React.FC<FormModalProps> = ({ isOpen, onClose, onSubmit }) => {
  const [formData, setFormData] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      await onSubmit(formData);
      onClose();
    } catch (error) {
      console.error('Form submission failed:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <form onSubmit={handleSubmit}>
        {/* Form content */}
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? 'Submitting...' : 'Submit'}
        </button>
      </form>
    </Modal>
  );
};

📚 Key Takeaways

What We Learned:

  1. Portal Rendering: How to render modals outside the component tree
  2. Focus Management: Trapping and restoring focus for accessibility
  3. Animation: Creating smooth enter/exit transitions
  4. Accessibility: Implementing proper ARIA attributes and keyboard navigation
  5. Event Handling: Managing backdrop clicks and escape key events

Best Practices:

  • Always use portals for modal rendering
  • Implement proper focus management
  • Add ARIA attributes for screen readers
  • Handle keyboard navigation (Escape key)
  • Prevent body scroll when modal is open
  • Clean up event listeners on unmount

Common Pitfalls to Avoid:

  • Not using portals (causes z-index issues)
  • Forgetting to restore focus on close
  • Missing ARIA attributes for accessibility
  • Not preventing body scroll
  • Memory leaks from uncleaned event listeners
  • Poor animation performance

🔗 Additional Resources


This modal component demonstrates essential frontend development skills including portal rendering, focus management, accessibility, and animation techniques. It's a great foundation for building more complex overlay components and understanding real-world development patterns.