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:
- Portal Rendering: Render modal outside main component tree
- Focus Management: Trap focus and restore on close
- Animation System: Smooth enter/exit transitions
- Accessibility: ARIA attributes and keyboard navigation
- 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:
- Modal state managed through context or props
- Portal renders modal at document body level
- Focus management handles keyboard navigation
- 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:
- Portal Rendering: How to render modals outside the component tree
- Focus Management: Trapping and restoring focus for accessibility
- Animation: Creating smooth enter/exit transitions
- Accessibility: Implementing proper ARIA attributes and keyboard navigation
- 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
- React Portal Documentation
- WAI-ARIA Dialog Pattern
- React Accessibility Guidelines
- CSS Animation Performance
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.