Portals

A component's output normally lands in the DOM under its parent's node. That breaks down for modals, dropdowns and tooltips: an ancestor with overflow: hidden, a stacking context or a CSS transform clips them or traps them behind other content. createPortal(children, domNode) renders children into any DOM node you choose, while leaving them where they were in the React 7,897 tree.

A modal rendered outside the app root that still bubbles into itHTMLLive
<div id="app-root"></div><div id="modal-root"></div>
<style>body{font:14px system-ui;margin:6px}
  #modal-root{border:2px dashed #6a1b9a;padding:2px 8px}</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react-dom": "https://esm.sh/react-dom@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
  import React, { useState } from 'react';
  import { createPortal } from 'react-dom';
  import { createRoot } from 'react-dom/client';
  const Modal = ({ children }) =>
    createPortal(children, document.getElementById('modal-root'));
  function App() {
    const [log, setLog] = useState('no click yet');
    return (
      <div onClick={(e) => setLog(`handled by App; DOM parent #${e.target.parentNode.id}`)}>
        <p>App root. Log: <b>{log}</b></p>
        <Modal><button>Click me (I live in #modal-root)</button></Modal>
      </div>);
  }
  createRoot(document.getElementById('app-root')).render(<App />);
  setTimeout(() => document.querySelector('#modal-root button').click(), 100);
</script>
Browser output of Listing 2.93
Browser output of 93

The button is a DOM child of #modal-root, a sibling of #app-root — yet clicking it ran the onClick handler on the <div> inside App. Events from a portal bubble along the React tree, not the DOM tree, so a portal's contents behave like children in every way that matters to your code: handlers on ancestors fire, context flows in, and state lifted above the portal still controls it. Only the DOM placement moves. That is what makes portals usable: a <Modal> sits next to the data that opens it and reads the same theme context, while its markup escapes to the end of <body> where no ancestor can clip it.