React Curriculum

Machine Coding: Production Modal Dialog Component

Hard100% Free~35 mins#react#machine-coding#modal#dialog#focus-trap#portals
Interview Answer (30-Second Summary)

A production Modal Dialog uses createPortal to escape parent stacking contexts, traps keyboard Tab focus, dismisses on Escape key or backdrop click, restores focus to the triggering element on unmount, and locks background document scrolling.

Key Learning Objectives
✓

Implement a production Modal Dialog with React Portal rendering into document.body.

✓

Build a robust focus trap algorithm that intercepts Tab and Shift+Tab keydown events.

✓

Implement background body scroll locking and restoration.

✓

Restore focus to the element that triggered the modal opening.

The Interview Problem & Context

Implement an accessible production-grade `<Modal isOpen={isOpen} onClose={handleClose} title="Title">Content</Modal>` component with Portal, focus trap, Escape dismissal, and background scroll lock.

1import React, { useEffect, useRef, ReactNode, KeyboardEvent } from 'react';
2import { createPortal } from 'react-dom';
3
4interface ModalProps {
5 isOpen: boolean;
6 onClose: () => void;
7 title: string;
8 children: ReactNode;
9}
10
11export function Modal({ isOpen, onClose, title, children }: ModalProps) {
12 const modalRef = useRef<HTMLDivElement>(null);
13 const triggerElementRef = useRef<HTMLElement | null>(null);
14
15 useEffect(() => {
16 if (!isOpen) return;
17
18 // 1. Save reference to previously active element
19 triggerElementRef.current = document.activeElement as HTMLElement;
20
21 // 2. Lock background scrolling
22 const prevOverflow = document.body.style.overflow;
23 document.body.style.overflow = 'hidden';
24
25 // 3. Focus first focusable element inside modal
26 const focusables = modalRef.current?.querySelectorAll<HTMLElement>(
27 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
28 );
29 if (focusables && focusables.length > 0) {
30 focusables[0].focus();
31 }
32
33 // 4. Teardown: Restore scroll and focus
34 return () => {
35 document.body.style.overflow = prevOverflow;
36 triggerElementRef.current?.focus();
37 };
38 }, [isOpen]);
39
40 const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
41 if (e.key === 'Escape') {
42 e.stopPropagation();
43 onClose();
44 return;
45 }
46
47 if (e.key === 'Tab') {
48 const focusables = modalRef.current?.querySelectorAll<HTMLElement>(
49 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
50 );
51 if (!focusables || focusables.length === 0) return;
52
53 const first = focusables[0];
54 const last = focusables[focusables.length - 1];
55
56 if (e.shiftKey) {
57 if (document.activeElement === first) {
58 e.preventDefault();
59 last.focus();
60 }
61 } else {
62 if (document.activeElement === last) {
63 e.preventDefault();
64 first.focus();
65 }
66 }
67 }
68 };
69
70 if (!isOpen) return null;
71
72 return createPortal(
73 <div
74 className="modal-backdrop"
75 onClick={onClose}
76 style={{
77 position: 'fixed',
78 inset: 0,
79 backgroundColor: 'rgba(0,0,0,0.5)',
80 display: 'flex',
81 alignItems: 'center',
82 justifyContent: 'center',
83 zIndex: 9999,
84 }}
85 >
86 <div
87 ref={modalRef}
88 role="dialog"
89 aria-modal="true"
90 aria-labelledby="modal-heading"
91 onClick={(e) => e.stopPropagation()}
92 onKeyDown={handleKeyDown}
93 className="modal-panel"
94 style={{
95 background: '#fff',
96 borderRadius: '8px',
97 padding: '24px',
98 maxWidth: '500px',
99 width: '90%',
100 boxShadow: '0 20px 25px -5px rgba(0,0,0,0.1)',
101 }}
102 >
103 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
104 <h2 id="modal-heading" style={{ margin: 0 }}>{title}</h2>
105 <button onClick={onClose} aria-label="Close modal">✕</button>
106 </div>
107 <div className="modal-content">{children}</div>
108 </div>
109 </div>,
110 document.body
111 );
112}
Deep Architectural Explanation

The Complete Architecture of an Accessible Modal

  1. Portals via createPortal:
    • Modals rendered inline in the DOM tree can be clipped by parent overflow: hidden or obscured by parent z-index stacking contexts.
    • createPortal(jsx, document.body) injects the DOM nodes directly under <body>, ensuring clean full-screen positioning.
  2. Keyboard Focus Trap:
    • Query all focusable elements: button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]).
    • When e.key === 'Tab':
      • If on the last element without Shift: e.preventDefault(); first.focus().
      • If on the first element with Shift: e.preventDefault(); last.focus().
  3. Focus Restoration:
    • Save document.activeElement before the modal mounts.
    • When the modal unmounts, call triggerElementRef.current?.focus(). This allows keyboard users to resume right where they left off.
  4. Event Propagation Defense:
    • The backdrop has onClick={onClose}.
    • The inner modal container must have onClick={(e) => e.stopPropagation()} so clicking inside the dialog body does not accidentally close the modal.
Common Interview Traps & Pitfalls
✕

Allowing focus to escape to background elements when the user tabs through the modal.

✕

Failing to stop propagation on modal clicks, causing the modal to close when clicking inside form inputs.

✕

Not restoring scroll overflow style on unmount, freezing the page scroll permanently.

Senior Interviewer Follow-Up Probes
?

How do you manage nested stacked modals without corrupting the focus restoration chain?

?

How would you animate modal entry and exit transitions with CSS or Framer Motion without breaking unmounting?

?

How does inert attribute in modern HTML simplify modal focus trapping?

Frequently Asked Interview Questions
Why must modal dialogs be rendered into a React Portal?

To break out of parent CSS overflow: hidden, transform, and z-index stacking contexts that would otherwise clip or layer over the modal.

Attach onClick={(e) => e.stopPropagation()} to the inner modal dialog element to stop the click event from bubbling to the backdrop.

It points to the ID of the modal title heading, enabling screen readers to announce the dialog title immediately upon opening.

By querying all focusable selectors inside the container and intercepting Tab keydown events to wrap focus between the first and last elements.

Set document.body.style.overflow = "hidden" when the modal opens, and restore the original overflow value in the useEffect cleanup function.

The HTML5 <dialog> element. Calling dialogElement.showModal() provides native focus trapping, top-layer rendering, backdrop styling, and Escape key dismissal.