- React
- Live Machine Coding Challenges and Custom Hooks
- Machine Coding: Production Modal Dialog Component
Machine Coding: Production Modal Dialog Component
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';34interface ModalProps {5 isOpen: boolean;6 onClose: () => void;7 title: string;8 children: ReactNode;9}1011export function Modal({ isOpen, onClose, title, children }: ModalProps) {12 const modalRef = useRef<HTMLDivElement>(null);13 const triggerElementRef = useRef<HTMLElement | null>(null);1415 useEffect(() => {16 if (!isOpen) return;1718 // 1. Save reference to previously active element19 triggerElementRef.current = document.activeElement as HTMLElement;2021 // 2. Lock background scrolling22 const prevOverflow = document.body.style.overflow;23 document.body.style.overflow = 'hidden';2425 // 3. Focus first focusable element inside modal26 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 }3233 // 4. Teardown: Restore scroll and focus34 return () => {35 document.body.style.overflow = prevOverflow;36 triggerElementRef.current?.focus();37 };38 }, [isOpen]);3940 const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {41 if (e.key === 'Escape') {42 e.stopPropagation();43 onClose();44 return;45 }4647 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;5253 const first = focusables[0];54 const last = focusables[focusables.length - 1];5556 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 };6970 if (!isOpen) return null;7172 return createPortal(73 <div74 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 <div87 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.body111 );112}
Deep Architectural Explanation
The Complete Architecture of an Accessible Modal
- Portals via createPortal:
- Modals rendered inline in the DOM tree can be clipped by parent
overflow: hiddenor obscured by parentz-indexstacking contexts. createPortal(jsx, document.body)injects the DOM nodes directly under<body>, ensuring clean full-screen positioning.
- Modals rendered inline in the DOM tree can be clipped by parent
- 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().
- If on the last element without Shift:
- Query all focusable elements:
- Focus Restoration:
- Save
document.activeElementbefore the modal mounts. - When the modal unmounts, call
triggerElementRef.current?.focus(). This allows keyboard users to resume right where they left off.
- Save
- 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.
- The backdrop has
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
How do you stop clicks inside the modal from closing it?
Attach onClick={(e) => e.stopPropagation()} to the inner modal dialog element to stop the click event from bubbling to the backdrop.
What is the purpose of aria-labelledby on a modal?
It points to the ID of the modal title heading, enabling screen readers to announce the dialog title immediately upon opening.
How does focus trapping work in pure JavaScript?
By querying all focusable selectors inside the container and intercepting Tab keydown events to wrap focus between the first and last elements.
How do you prevent body scrolling when the modal opens?
Set document.body.style.overflow = "hidden" when the modal opens, and restore the original overflow value in the useEffect cleanup function.
What is the native HTML5 alternative to custom modal components?
The HTML5 <dialog> element. Calling dialogElement.showModal() provides native focus trapping, top-layer rendering, backdrop styling, and Escape key dismissal.
