Skip to the solution
HardFrontend7 min read

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

888 views

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.

  • portals
  • dom
  • ui
  • advanced

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA modal escaping overflow:hidden, and a click bubbling to its React parent
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 83 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

How can the `key` prop be used to deliberately reset component state?

Continue