Skip to content
intermediatePhase 39 · Accessibility

Focus Management

Manage focus for modals, route changes, and dynamic content.

30m
0 problems
Topic Progress0%

Focus Trapping

Focus Trapping

Keep keyboard focus within a component (modals, drawers).

Focus Trap Hook

// hooks/useFocusTrap.js
function useFocusTrap(isActive) {
  const containerRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (!isActive) return;

    // Remember previous focus
    previousFocusRef.current = document.activeElement;

    // Get focusable elements
    const container = containerRef.current;
    const focusable = container.querySelectorAll(
      'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
    );

    const firstElement = focusable[0];
    const lastElement = focusable[focusable.length - 1];

    // Focus first element
    firstElement?.focus();

    const handleKeyDown = (e) => {
      if (e.key !== 'Tab') return;

      if (e.shiftKey) {
        // Shift+Tab: if at first, go to last
        if (document.activeElement === firstElement) {
          e.preventDefault();
          lastElement?.focus();
        }
      } else {
        // Tab: if at last, go to first
        if (document.activeElement === lastElement) {
          e.preventDefault();
          firstElement?.focus();
        }
      }
    };

    container.addEventListener('keydown', handleKeyDown);

    return () => {
      container.removeEventListener('keydown', handleKeyDown);
      // Restore previous focus
      previousFocusRef.current?.focus();
    };
  }, [isActive]);

  return containerRef;
}

Modal Component

function Modal({ isOpen, onClose, title, children }) {
  const containerRef = useFocusTrap(isOpen);

  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        ref={containerRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        onClick={(e) => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close modal">
          ×
        </button>
      </div>
    </div>
  );
}

Accessible Dialog

function ConfirmDialog({ isOpen, onConfirm, onCancel, message }) {
  const confirmRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      confirmRef.current?.focus();
    }
  }, [isOpen]);

  return (
    <Modal isOpen={isOpen} onClose={onCancel} title="Confirm Action">
      <p>{message}</p>
      <div className="dialog-actions">
        <button onClick={onCancel}>Cancel</button>
        <button ref={confirmRef} onClick={onConfirm}>
          Confirm
        </button>
      </div>
    </Modal>
  );
}

Modal Focus

Modal Focus

Handle focus properly in modal dialogs.

Modal Focus Management

function Dialog({ isOpen, onClose, children }) {
  const dialogRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Save current focus
      previousFocusRef.current = document.activeElement;
      
      // Prevent body scroll
      document.body.style.overflow = 'hidden';
      
      // Add aria-hidden to background
      const mainContent = document.getElementById('main-content');
      if (mainContent) {
        mainContent.setAttribute('aria-hidden', 'true');
        mainContent.setAttribute('inert', '');
      }
    }

    return () => {
      document.body.style.overflow = '';
      const mainContent = document.getElementById('main-content');
      if (mainContent) {
        mainContent.removeAttribute('aria-hidden');
        mainContent.removeAttribute('inert');
      }
      previousFocusRef.current?.focus();
    };
  }, [isOpen]);

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') {
      onClose();
    }
  };

  if (!isOpen) return null;

  return (
    <div className="dialog-overlay" role="dialog" aria-modal="true">
      <div
        ref={dialogRef}
        className="dialog-content"
        onKeyDown={handleKeyDown}
      >
        {children}
      </div>
    </div>
  );
}

Focus Restoration

// Store focus reference
const focusHistory = [];

function pushFocus(element) {
  focusHistory.push(document.activeElement);
  element?.focus();
}

function popFocus() {
  const previous = focusHistory.pop();
  previous?.focus();
}

// Usage
function App() {
  const [modalOpen, setModalOpen] = useState(false);

  const openModal = () => {
    pushFocus(document.getElementById('open-button'));
    setModalOpen(true);
  };

  const closeModal = () => {
    setModalOpen(false);
    popFocus();
  };

  return (
    <div>
      <button id="open-button" onClick={openModal}>
        Open Modal
      </button>
      <Modal isOpen={modalOpen} onClose={closeModal} />
    </div>
  );
}

Route Change Focus

Route Change Focus

Manage focus when navigating between pages.

Announce Route Changes

// Live region for announcements
function RouteAnnouncer() {
  const [announcement, setAnnouncement] = useState('');
  const location = useLocation();

  useEffect(() => {
    // Announce new page
    setAnnouncement('');
    const timer = setTimeout(() => {
      const title = document.title || 'New page';
      setAnnouncement(`Navigated to ${title}`);
    }, 100);

    return () => clearTimeout(timer);
  }, [location.pathname]);

  return (
    <div
      role="status"
      aria-live="polite"
      aria-atomic="true"
      className="sr-only"
    >
      {announcement}
    </div>
  );
}

Focus on Navigation

// Focus main content on route change
function PageWrapper({ children }) {
  const mainRef = useRef(null);
  const location = useLocation();

  useEffect(() => {
    // Focus main content on route change
    mainRef.current?.focus();
    
    // Scroll to top
    window.scrollTo(0, 0);
  }, [location.pathname]);

  return (
    <main
      ref={mainRef}
      tabIndex={-1}
      style={{ outline: 'none' }}
    >
      {children}
    </main>
  );
}

Skip Navigation

function Layout({ children }) {
  return (
    <div>
      {/* Skip link */}
      <a href="#main-content" className="skip-link">
        Skip to main content
      </a>
      
      <header>
        <nav aria-label="Main">{/* ... */}</nav>
      </header>
      
      <main id="main-content" tabIndex={-1}>
        {children}
      </main>
    </div>
  );
}

// CSS for skip link
.skip-link {
  position: absolute;
  left: -9999px;
}

.skip-link:focus {
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  background: white;
  padding: 8px 16px;
  border: 2px solid black;
}

Focus Management Checklist

  • Modal traps focus
  • Focus returns to trigger on close
  • Route changes announce to screen readers
  • Skip links provided
  • Focus visible on interactive elements
  • No keyboard traps

Practice Problems

0/3solved
Build Focus Management Component

Create a reusable React component implementing Focus Management. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Focus Management Testing

Write unit and integration tests for Focus Management using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Focus Management Performance

Optimize Focus Management for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What is focus trapping?

Question 1 options

2. Where should focus go when a modal closes?

Question 2 options

3. What is the primary purpose of Focus Management?

Question 3 options

4. What is a common mistake when implementing Focus Management?

Question 4 options

Flashcards

Question

What is focus trapping?

Answer

Keeping keyboard focus within a component (like a modal) until it's dismissed.

Question

Why restore focus on modal close?

Answer

To maintain context and good UX for keyboard users.

Question

What is route change focus management?

Answer

Focusing the main content and announcing the new page to screen readers.

Question

What is a skip link?

Answer

A link that lets keyboard users skip navigation and go directly to main content.

Question

What is Focus Management?

Answer

Focus Management is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Trap focus in modals and dialogs
  • 2.Restore focus when components close
  • 3.Announce route changes to screen readers
  • 4.Use skip links for keyboard navigation
  • 5.Manage focus for dynamic content

Interview Tips

  • Explain how focus trapping works
  • Discuss focus management in SPAs
  • Know how to implement accessible modals

Cheat Sheet

Focus Management Cheat Sheet

Focus Trap

  • Save previous focus
  • Find focusable elements
  • Handle Tab/Shift+Tab
  • Restore focus on unmount

Modal Focus

  • Trap focus inside
  • Prevent body scroll
  • aria-hidden background
  • Restore focus on close

Route Changes

  • Announce to screen readers
  • Focus main content
  • Scroll to top
  • Skip link support