Skip to solution
hardFrontend

What are Portals in React and when would you use them?

888 views
01

Understand the problem

Question presented to candidate: "Your modal is being clipped by a parent with overflow: hidden. How do portals solve that, and what do they not change?"

What a strong answer should cover:

  • createPortal(children, domNode) renders children into a different DOM container while keeping them in the same place in the React tree.
  • That split is the whole feature: DOM position changes, React position does not.
  • It solves CSS containment problems — overflow: hidden, z-index stacking contexts, transform creating a new containing block — which no amount of z-index tinkering fixes from inside.
  • Events still bubble through the React tree, not the DOM tree. A click inside the portal reaches the React parent's onClick even though the DOM parent is elsewhere. This surprises people and is usually what you want.
  • Context, state and props all flow normally, because nothing about the React tree changed.
  • Typical uses: modals, dialogs, tooltips, toasts, dropdown menus, anything that must escape its container.
  • It is not an isolation mechanism — that is shadow DOM. Styles and events still apply.
  • Accessibility is not solved for you: focus trapping, aria-modal, restoring focus on close, and Escape handling are still your job. Prefer <dialog> or a headless library.

Clarifying questions expected:

  • "Is the problem CSS containment, or DOM ordering for accessibility?" — portals fix the first; the second needs more.
  • "Does this need focus management?" — almost always yes for modals.

Code / implementation expected: Optional. Showing the event bubbling through the React parent is the non-obvious part worth demonstrating.

portalsdomuiadvanced
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes JSX and event basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The DOM placement and the event ordering in sections 3 and 4 were **executed a

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A modal escaping overflow:hidden, and a click bubbling to its React parent
Run Playground
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";

// The portal target: created once, appended to the body, so nothing in the
// app can ever become its containing block.
function usePortalTarget() {
  const ref = useRef(null);
  const [, ready] = useState(0);
  useEffect(() => {
    const el = document.createElement("div");
    el.id = "modal-root";
    document.body.appendChild(el);
    ref.current = el;
    ready(1);                       // re-render now that the target exists
    return () => { document.body.removeChild(el); };
  }, []);
  return ref.current;
}

function Box({ portalled, target, onLog }) {
  return (
    // ⚠️ This parent clips everything inside it. That is the problem portals fix.
    <div
      style={{
        border: "2px solid #c66", borderRadius: 8, padding: 12,
        height: 90, overflow: "hidden", position: "relative",
      }}
      onClick={() => onLog("2. React parent onClick — even for the portal")}
    >
      <div style={{ fontSize: 13 }}>
        a container with <code>overflow: hidden</code>
      </div>

      {(() => {
        const panel = (
          <div
            onClick={() => onLog("1. panel onClick (inside the portal)")}
            style={{
              position: portalled ? "fixed" : "absolute",
              top: portalled ? "50%" : 60,
              left: portalled ? "50%" : 10,
              transform: portalled ? "translate(-50%, -50%)" : "none",
              background: "#fff", border: "2px solid #4f46e5",
              borderRadius: 8, padding: 12, fontSize: 13, zIndex: 10,
            }}
          >
            {portalled ? "✅ portalled — fully visible" : "❌ not portalled — clipped"}
            <div><button>click me</button></div>
          </div>
        );
        return portalled && target ? createPortal(panel, target) : panel;
      })()}
    </div>
  );
}

export default function App() {
  const target = usePortalTarget();
  const [portalled, setPortalled] = useState(false);
  const [log, setLog] = useState([]);
  const push = (s) => setLog((l) => [...l, s].slice(-4));

  // A NATIVE listener on the portal target, to show both trees at once.
  useEffect(() => {
    if (!target) return;
    const onNative = () => push("3. native listener on the portal target (DOM tree)");
    target.addEventListener("click", onNative);
    return () => target.removeEventListener("click", onNative);
  }, [target]);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        <button onClick={() => setPortalled((p) => !p)}>
          {portalled ? "stop portalling" : "portal it"}
        </button>
        <button onClick={() => setLog([])}>clear log</button>
      </div>

      <Box portalled={portalled} target={target} onLog={push} />

      <pre style={{ background: "#f6f6f8", padding: 10, borderRadius: 8, fontSize: 12, marginTop: 12, minHeight: 84 }}>
{log.length ? log.join("\n") : "click the panel"}
      </pre>

      <p style={{ fontSize: 13, color: "#666" }}>
        Unportalled, the panel is cut off by the red container. Portalled, it
        escapes entirely — and clicking it still runs the red container's{" "}
        <code>onClick</code>, because React propagates through the component
        tree. The third line only appears when portalled: that is the real DOM
        bubbling to the container the node actually lives in.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 83 of 119 decoded in the React.js track. One more won't hurt.

Back to track