Front-end Engineering Lab
RADIO FrameworkUI Components

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:

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" or role="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:

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:

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

On this page