Modal/Dialog Component
System design for accessible modal with focus trap, keyboard shortcuts, and animations
Design an accessible modal component with focus management, keyboard shortcuts, and smooth animations.
R - Requirements
Key Questions:
- Fixed or responsive size?
- Multiple modals (stack)?
- Close on outside click?
- Close on ESC key?
- Accessibility level? (WCAG AA/AAA)
- Dismissible? (yes/no)
Common Answers:
- Responsive with max-width
- Stack support for nested modals
- Close on outside click + ESC
- WCAG AA minimum
A - Architecture
Modal Lifecycle:
Modal Stack Architecture:
Architecture Decisions:
- Portal rendering (render outside DOM hierarchy) - Prevents z-index issues, allows stacking
- Focus trap (keep focus inside modal) - Accessibility requirement, cycles with Tab
- Scroll lock (prevent body scroll when open) - Prevents background scrolling
- Z-index management (for stacked modals) - Increment z-index for each modal
- Backdrop/overlay (semi-transparent background) - Visual separation, click to close
Relevant Content:
- Focus Management SPA - Focus trap patterns
- ARIA Live Regions - Announcements
D - Data Model
State Management:
- Modal open/closed state
- Modal stack (for multiple modals)
- Focus history (restore focus on close)
- Scroll position (restore on close)
Implementation Example - Focus Trap:
function trapFocus(modalElement: HTMLElement) {
const focusableElements = modalElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
function handleTab(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
modalElement.addEventListener('keydown', handleTab);
firstElement.focus();
return () => {
modalElement.removeEventListener('keydown', handleTab);
};
}
// Scroll lock
function lockBodyScroll() {
document.body.style.overflow = 'hidden';
}
function unlockBodyScroll() {
document.body.style.overflow = '';
}Persistence:
- None (modals are ephemeral)
I - Interface
Features:
- Overlay/backdrop (semi-transparent)
- Modal container (centered, responsive)
- Close button (X, top-right)
- Focus trap (tab cycles inside modal)
- Keyboard shortcuts (ESC to close, Tab navigation)
- Animations (fade in/out, slide, scale)
Accessibility:
- ARIA modal pattern
role="dialog"orrole="alertdialog"aria-modal="true"aria-labelledby(title)aria-describedby(description)- Focus trap (tab stays inside)
- Focus restoration (return focus to trigger)
- Screen reader announcements
- Keyboard shortcuts (ESC, Tab, Shift+Tab)
Relevant Content:
- Accessible Animations - Motion accessibility
- Keyboard Shortcuts - ESC handling
- ARIA Live Regions - Announcements
O - Optimizations
Performance:
- Lazy render content (only render when open)
- Animation performance (use transform/opacity, not layout properties)
- Memory cleanup (remove event listeners on close)
- Portal optimization (render in document.body)
UX:
- Smooth animations (60fps, GPU accelerated)
- Focus management (prevent focus loss)
- Scroll lock (prevent body scroll)
Relevant Content:
- Animation Performance - 60fps animations
- Composite Layers - GPU acceleration
- Paint Optimization - Reduce paint
Implementation Checklist
- Portal rendering (document.body)
- Focus trap (tab cycles inside)
- Focus restoration (return to trigger)
- Scroll lock (prevent body scroll)
- Keyboard shortcuts (ESC to close, Tab to navigate)
- Click outside to close (optional)
- Animations (fade, slide, scale)
- ARIA attributes (role, aria-modal, aria-labelledby, aria-describedby)
- Screen reader support
- Stack support (multiple modals, z-index management)
- Backdrop/overlay
- Responsive sizing
Common Pitfalls
❌ No focus trap → Users tab outside modal, lose context
✅ Trap focus inside modal, cycle with Tab, restore on close
❌ No focus restoration → Focus lost after closing
✅ Save focus before opening, restore on close
❌ Janky animations → Poor UX
✅ Use transform/opacity, GPU acceleration, 60fps
❌ No keyboard support → Poor accessibility
✅ ESC to close, Tab to navigate, Enter to confirm
❌ Body scroll not locked → Background scrolls when modal open
✅ Lock body scroll when modal opens, unlock on close