# Machine-Coding: Build a Modal Manager

Source: https://www.techinterview.org/post/3233474944/machine-coding-modal-manager/
Updated: 2026-07-12 · techinterview.org

The modal manager round tests whether the candidate can build a robust, accessible modal system from scratch. Modals are deceptively complex: stacking multiple modals, returning focus to the originating element on close, trapping focus inside the modal, ESC handling, scroll lock, and accessibility-correct ARIA attributes are all part of the round.

This piece walks through the implementation patterns, the accessibility requirements, and what interviewers grade.

## The typical prompt

- "Build a modal component. Multiple modals can be open simultaneously, stacked."

- "Implement a modal manager with React Portals."

- "Build a confirmation dialog system."

## What interviewers grade

- The modal opens, shows over the content, and can be closed.

- The ESC key closes the topmost modal, not all of them.

- Focus moves into the modal on open and returns to the trigger on close.

- Tab cycles within the modal and doesn't escape to background content.

- Clicking the backdrop outside the modal usually closes it, though this is configurable.

- The page behind doesn't scroll while a modal is open.

- Multiple modals stack correctly with z-index.

- The dialog exposes the ARIA dialog role, aria-modal, and aria-labelledby for the title.

## Implementation: a robust pattern

### Step 1: portals and the modal root


```
function Modal({ isOpen, onClose, title, children }) {
  if (!isOpen) return null;
  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div
        className="modal-dialog"
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        onClick={e => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}
```


The portal pulls the modal out of the React tree's DOM hierarchy, attaching it to a known container. This avoids CSS issues with overflow:hidden ancestors and z-index stacking.

### Step 2: ESC handling


```
useEffect(() => {
  if (!isOpen) return;
  const handler = (e) => {
    if (e.key === 'Escape') onClose();
  };
  document.addEventListener('keydown', handler);
  return () => document.removeEventListener('keydown', handler);
}, [isOpen, onClose]);
```


For stacked modals, only the topmost should respond to ESC. The cleanest pattern uses a stack in a context provider; the topmost modal subscribes to keyboard events.

### Step 3: focus management


```
const dialogRef = useRef(null);
const previouslyFocusedRef = useRef(null);

useEffect(() => {
  if (!isOpen) return;
  // Save what was focused before
  previouslyFocusedRef.current = document.activeElement;
  // Focus the dialog (or first focusable element inside)
  dialogRef.current?.focus();
  return () => {
    // Restore focus on unmount
    previouslyFocusedRef.current?.focus();
  };
}, [isOpen]);
```


The dialog itself needs `tabIndex={-1}` to be focusable.

### Step 4: focus trap

When the user presses Tab, focus must cycle within the modal, not escape to background content.


```
function handleKeyDown(e) {
  if (e.key !== 'Tab') return;
  const focusable = dialogRef.current.querySelectorAll(
    'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault();
    last.focus();
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault();
    first.focus();
  }
}
```


This is a hand-rolled focus trap. In production, use the focus-trap library. For the interview, demonstrating that you know the concept and can implement it is the bar.

### Step 5: body scroll lock


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


For stacked modals, the lock should be applied once for any open modal and released only when all are closed. The cleanest implementation uses a counter in a context provider.

## Stacking modals

To support multiple modals stacking, hoist the management into a context:


```
const ModalContext = createContext();

function ModalProvider({ children }) {
  const [stack, setStack] = useState([]);
  const open = (config) => setStack(s => [...s, { id: Date.now(), ...config }]);
  const close = (id) => setStack(s => s.filter(m => m.id !== id));

  return (
    <ModalContext.Provider value={{ open, close }}>
      {children}
      {stack.map((m, i) => (
        <Modal key={m.id} {...m} onClose={() => close(m.id)} isTop={i === stack.length - 1} />
      ))}
    </ModalContext.Provider>
  );
}
```


The `isTop` flag tells each modal whether it's the topmost (only that one responds to ESC, manages focus on close, etc.).

## Common pitfalls

- Forgetting focus restoration breaks the user's tab order after the modal closes.

- With no focus trap, Tab escapes to background content and screen readers wander out.

- ESC closing all modals at once, when it should close only the topmost.

- Backdrop clicks close the modal even when child clicks bubble up; use stopPropagation on the dialog content.

- With no body scroll lock, the background scrolls behind the modal.

- Z-index hell: portals avoid most z-index issues, but stacking modals still need explicit z-index management.

- Combining aria-modal=true with aria-hidden=false on the background is wrong; aria-modal already implies the background should be inert.

## Stretch goals

- Animated open / close transitions.

- Confirmation dialog API: `const result = await confirm({ title, message })`.

- Different modal sizes / variants (alert, confirm, custom).

- Modal scrolling when content overflows.

- Focus return when user closes via Tab+Escape.

## Time budget for a 45-minute round

- 0-5 min: clarify requirements (stacking? animations? confirmation API?).

- 5-15 min: basic modal with portal, open/close, ESC handling.

- 15-25 min: focus management and focus trap.

- 25-35 min: body scroll lock, backdrop click, accessibility attributes.

- 35-40 min: stacking with context provider.

- 40-45 min: polish, edge cases.

## Frequently Asked Questions

### Should I use a library?

For the round, no. In production, Radix UI's Dialog or React Aria's Dialog are excellent choices.

### Is the focus trap really necessary?

For accessibility yes. WCAG 2.1 requires it for modal dialogs. Senior+ rounds grade for it.

### What about the inert attribute for background content?

The HTML inert attribute now exists and can be used for modal backgrounds. Newer pattern; not yet universal but worth knowing for senior+ candidates.

### How does this differ from a popover or tooltip?

Modals trap focus and demand interaction; popovers and tooltips don't. The aria-modal attribute distinguishes them. Different patterns; don't conflate.

### Are nested modals a real-world need?

Sometimes — confirm dialogs spawned from inside a modal, for instance. Many design teams discourage them; the pattern still needs to be supported.
