CPCODELAB
React

Portals — Rendering Outside the React Root

9 min read

Escaping the DOM Hierarchy

Portals render a child tree into a different DOM node than the component's parent. Despite living elsewhere in the DOM, the portal still behaves like a normal React child — events bubble up through the React component tree, not the DOM tree.

Full modal implementation

jsx
import { createPortal } from "react-dom";
import { useEffect, useRef } from "react";

function Modal({ title, children, onClose }) {
  const dialogRef = useRef(null);

  useEffect(() => {
    // Trap focus inside modal
    dialogRef.current?.focus();

    function handleKey(e) {
      if (e.key === "Escape") onClose();
    }
    document.addEventListener("keydown", handleKey);
    return () => document.removeEventListener("keydown", handleKey);
  }, [onClose]);

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      aria-label={title}
      ref={dialogRef}
      tabIndex={-1}
      className="modal-overlay"
      onClick={onClose}
    >
      <div className="modal-box" onClick={e => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>✕</button>
        <h2>{title}</h2>
        {children}
      </div>
    </div>,
    document.body
  );
}

Event bubbling through portals

Even though the portal DOM is a sibling of <div id="root">, a click inside the portal bubbles through the React tree — so an onClick on a parent React component will still fire. This is usually what you want, but keep it in mind for click-outside-to-close logic.

Always manage focus trapping and keyboard dismissal in modals for accessibility. The dialog HTML element (with showModal()) is increasingly supported and handles much of this natively without a portal.

Ready to test yourself?
Practice React— quiz & coding exercises