React Portals

React Portals render a component's output into a different DOM node than its parent — while still keeping it as a logical child in the React component tree. Portals are the solution when you need elements like modals, tooltips, and dropdown menus to visually escape their container but still behave as React children.

The Problem Portals Solve

CSS properties like overflow: hidden, z-index, and position: relative on a parent element affect all children. A modal rendered inside a card component gets clipped by the card's overflow: hidden rule.

Diagram: The clipping problem

<div style="overflow: hidden; height: 200px">   ← Card container
  <h3>Product Name</h3>
  <p>Description</p>
  <Modal />   ← Modal is INSIDE the card
              The modal gets cut off at 200px height!
              overflow: hidden clips it.
</div>

Diagram: With a Portal

<div style="overflow: hidden; height: 200px">   ← Card container
  <h3>Product Name</h3>
  <p>Description</p>
  {/* Modal renders here in React logic... */}
</div>

<div id="modal-root">   ← Appended to document.body
  <Modal />   ← Portal sends Modal here in the DOM
              No overflow clipping, full-screen overlay works!
</div>

Creating a Portal

Use ReactDOM.createPortal(children, domNode). The first argument is the JSX to render. The second is the DOM element to render it into.

import ReactDOM from "react-dom";

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return ReactDOM.createPortal(
    <div style={{
      position: "fixed",
      top: 0, left: 0, right: 0, bottom: 0,
      backgroundColor: "rgba(0, 0, 0, 0.5)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      zIndex: 1000,
    }}>
      <div style={{
        background: "white",
        padding: "32px",
        borderRadius: "8px",
        minWidth: "320px",
      }}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.body  // Render directly into document.body
  );
}

Setting Up a Dedicated Portal Container

Add a dedicated container div in your HTML file for portals. This keeps portal-rendered content organized and separate from the main React root.

<!-- index.html -->
<body>
  <div id="root"></div>       <!-- Main React app -->
  <div id="modal-root"></div> <!-- Portal target -->
</body>
// Use the dedicated container
return ReactDOM.createPortal(
  <div className="modal-overlay">...</div>,
  document.getElementById("modal-root")
);

Using the Modal

function ProductCard({ product }) {
  const [showModal, setShowModal] = useState(false);

  return (
    <div style={{ overflow: "hidden", height: "150px" }}>
      <h3>{product.name}</h3>
      <button onClick={() => setShowModal(true)}>View Details</button>

      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <p>Price: ${product.price}</p>
      </Modal>
    </div>
  );
}

The Modal component is a React child of ProductCard, but its DOM output lands in document.body — completely outside the card's overflow container.

Events Still Bubble Through the React Tree

Even though a portal renders outside the parent DOM, events inside the portal bubble up through the React component tree as if they were inside the parent. This keeps React's event system consistent.

function App() {
  const handleClick = () => {
    console.log("App clicked"); // This fires when modal content is clicked!
  };

  return (
    <div onClick={handleClick}>
      <Modal /> {/* Modal is a portal, but click events bubble to App */}
    </div>
  );
}

This behavior is intentional. It means you can handle portal events in parent components just like any other child.

Common Portal Use Cases

Use Case               Why Portal Helps
-----------------------------------------------------
Modal / Dialog         Escape overflow:hidden on parent containers
Tooltip                Position above all other elements with z-index
Dropdown Menu          Avoid clipping by scrollable containers
Toast Notifications    Always appear at edge of screen regardless of page structure
Drawer / Side Panel    Full-height overlay outside layout constraints

Reusable Portal Component

import { useEffect, useState } from "react";
import ReactDOM from "react-dom";

function Portal({ children, containerId = "portal-root" }) {
  const [container, setContainer] = useState(null);

  useEffect(() => {
    let el = document.getElementById(containerId);

    if (!el) {
      el = document.createElement("div");
      el.id = containerId;
      document.body.appendChild(el);
    }

    setContainer(el);

    return () => {
      // Cleanup: remove dynamically created container
      if (el.childNodes.length === 0) {
        document.body.removeChild(el);
      }
    };
  }, [containerId]);

  if (!container) return null;

  return ReactDOM.createPortal(children, container);
}

// Usage
function Tooltip({ text, children }) {
  const [visible, setVisible] = useState(false);

  return (
    <span
      onMouseEnter={() => setVisible(true)}
      onMouseLeave={() => setVisible(false)}
    >
      {children}
      {visible && (
        <Portal>
          <div style={{ position: "fixed", background: "#333", color: "#fff", padding: "6px 10px", borderRadius: "4px" }}>
            {text}
          </div>
        </Portal>
      )}
    </span>
  );
}

Closing a Modal on Escape Key

Add keyboard support to portals for accessibility.

useEffect(() => {
  if (!isOpen) return;

  const handleKeyDown = (e) => {
    if (e.key === "Escape") onClose();
  };

  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);

Summary

Portals render JSX into a DOM node outside the parent component's DOM hierarchy while keeping the component logically inside the React tree. Use ReactDOM.createPortal(jsx, domNode) to create one. Add a dedicated portal-root div in your HTML for clean organization. Events from portal content bubble through the React tree normally. Portals solve clipping and stacking context problems for modals, tooltips, dropdowns, and overlays — any UI that must visually escape its container's CSS constraints.

Leave a Comment

Your email address will not be published. Required fields are marked *