← Back to React Course | Chapter 13: Advanced React & Architecture | Lesson 2 of 14

React Portals

A React Portal is like a secret tunnel that lets a component appear in a totally different room of the house, even though it's still officially controlled from its original room.

Why Portals Exist: The Overflow/Z-Index Problem

A modal nested deep inside a component with 'overflow: hidden' or a specific stacking context can get visually clipped or hidden behind other elements, no matter how high you set its z-index. Portals solve this by rendering the modal's actual DOM elsewhere in the page, escaping that parent's CSS constraints entirely.

Note: A common setup is a dedicated '#modal-root' <div> placed directly under <body> in your HTML, completely outside your main app's DOM structure.

Warning: Without a portal, a deeply-nested modal can be invisibly clipped by an ancestor's overflow or stacking rules, even though the code looks correct.

Example: Why Portals Exist: The Overflow/Z-Index Problem

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function Modal({ children }) {
  return ReactDOM.createPortal(
    <div style={{ position: "fixed", top: 20, left: 20, background: "white", border: "1px solid #333", padding: "16px" }}>
      {children}
    </div>,
    document.body
  );
}
function App() {
  return <div><p>Main app content</p><Modal><p>I'm rendered via a portal, into document.body</p></Modal></div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Portals Still Follow the React Tree for Events

Even though a portal's actual DOM elements live somewhere else in the page, React still treats them as part of the original component tree for props, context, and event bubbling. A click inside a portal's content will still bubble up through its REACT ancestors, not its DOM ancestors.

Note: This means Context values from a parent still flow correctly into a portal's content, exactly as if it hadn't been portaled at all.

Warning: Don't assume clicking outside a portal (in the DOM sense) means clicking outside it in React's event system — the event bubbling follows React's tree, which can surprise you.

Example: Portals Still Follow the React Tree for Events

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
const ThemeContext = React.createContext("light");
function Modal({ children }) {
  return ReactDOM.createPortal(<div style={{border: "1px solid #333", padding: "8px"}}>{children}</div>, document.body);
}
function ModalContent() {
  const theme = React.useContext(ThemeContext);
  return <p>Theme inside portal: {theme}</p>;
}
function App() {
  return <ThemeContext.Provider value="dark"><Modal><ModalContent /></Modal></ThemeContext.Provider>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

The Target Node Must Already Exist

createPortal's second argument must be a real, already-existing DOM node — React doesn't create it for you. This target is usually a plain <div> already present in your HTML (like index.html), separate from React's own root element.

Note: document.body itself is a valid, always-existing target if you don't want to add a separate dedicated div.

Warning: Passing a DOM node that doesn't exist yet (like one that hasn't rendered, or a typo'd ID) throws an error the moment createPortal tries to use it.

Example: The Target Node Must Already Exist

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function TooltipPortal({ children }) {
  return ReactDOM.createPortal(
    <div style={{ position: "fixed", bottom: 20, right: 20, background: "black", color: "white", padding: "8px" }}>
      {children}
    </div>,
    document.body // always exists, safe target
  );
}
function App() {
  return <TooltipPortal><span>I'm a portaled tooltip</span></TooltipPortal>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Forgetting the target DOM node passed to createPortal must actually exist in the HTML before the portal tries to render into it.
  2. Assuming a portal breaks React's component tree for events too — event bubbling still follows the React tree, not the DOM tree.
  3. Creating a brand-new target DOM node on every render instead of reusing a stable one.
Chapter Summary
  • ReactDOM.createPortal(children, domNode) renders children into a different part of the actual DOM.
  • This is commonly used for modals, tooltips, and dropdowns that need to escape a parent's overflow:hidden or z-index stacking context.
  • Despite rendering elsewhere in the DOM, a portal's content still behaves as a normal part of the React component tree for state, context, and event bubbling.
  • The target DOM node must already exist (usually a dedicated <div> in index.html) before createPortal can render into it.
Browser Support

Available since React 16.0 (Portals introduced).

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.