Building a Modal as a Layout Component

A modal is not a special case. It's a layout component with visibility state. Build one from scratch and discover why stopPropagation is the one line you cannot skip.

June 27, 20263 min read3 / 3

Lists showed us in the previous post that layout thinking scales to data-driven components. Modals prove it works for UI chrome too.

A modal is just a layout component with one extra piece: visibility state. It wraps arbitrary content and controls whether that content is visible. The content it wraps has no idea a modal exists.

The Structure

JSX
const Modal = ({ children }) => { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(true)}>Open</button> {show && ( <ModalBackground onClick={() => setShow(false)}> <ModalContent onClick={e => e.stopPropagation()}> <button onClick={() => setShow(false)}>Close</button> {children} </ModalContent> </ModalBackground> )} </> ); };

ModalBackground covers the screen. Clicking it closes the modal. ModalContent holds the actual content.

The e.stopPropagation() on ModalContent is the line you cannot skip. Without it, clicking anything inside the modal content bubbles up to ModalBackground and closes the modal immediately.

Click inside the content. Event bubbles up. Background closes. One confusing bug.

stopPropagation cuts the bubble before it reaches the background.

The Styled Components

JSX
const ModalBackground = styled.div` position: absolute; left: 0; top: 0; overflow: auto; background-color: rgba(0, 0, 0, 0.5); width: 100%; height: 100%; `; const ModalContent = styled.div` margin: 12% auto; padding: 24px; background-color: white; width: 50%; `;

ModalBackground takes the full screen with a semi-transparent dark overlay. ModalContent is centered and white.

The Payoff: Composition

Here is where layout component thinking pays off. The same DetailedUserRow from the list post works inside the modal:

JSX
<Modal> <DetailedUserRow user={users[0]} /> </Modal>

Modal knows nothing about users. DetailedUserRow knows nothing about modals. Neither needs to change when the other changes.

This is the rule that connects all three patterns in this chapter. The split screen does not know what it arranges. The list does not know what each row displays. The modal does not know what it wraps.

Each layout component has zero knowledge of its contents. Each content component has zero knowledge of its location. That independence is what makes them composable.

The Essentials

  1. A modal is a layout component. Its job is visibility control, not content rendering.
  2. ModalBackground gets a click handler to close. ModalContent gets stopPropagation to block that close from firing on inner clicks.
  3. The children prop keeps the modal completely unaware of what it contains.
  4. Any content component from anywhere in your app can go inside a modal without modification.
  5. Separation of layout and content makes both sides easier to change independently.

Further Reading and Watching

  • Layout Components | React Design Pattern by Coding With Mr.M. Shows how layout components connect across a full app.
  • React docs: Portals covers the production approach to modals: rendering outside the DOM hierarchy using createPortal. This is what you reach for after mastering the basic pattern.

Practice what you just read.

Modal Pattern Quiz
1 exercise