Portals

Reviewed & published by Brayan K

A portal lets a component render its output into a different part of the DOM — typically a node at the bottom of <body> — while keeping it a normal child of your React tree. It's the standard tool for modals, tooltips, toasts, and dropdowns: anything that must visually escape a parent's overflow, z-index, or transform without losing its place in your component logic.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ The Problem Portals Solve

Picture a modal rendered inside a sidebar card that has overflow: hidden. Your full-screen overlay gets clipped to the card's edges. Or a parent has z-index: 1 and your modal can't rise above the header. Portals fix this by moving the DOM output elsewhere:

import { createPortal } from "react-dom";

// index.html has: <div id="modal-root"></div> just before </body>
function Modal({ children, onClose }) {
  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")   // ← rendered HERE, not inline
  );
}

// Used deep inside a clipped card — but the overlay still covers the screen:
function SettingsCard() {
  const [open, setOpen] = useState(false);
  return (
    <div className="card" style={{ overflow: "hidden" }}>
      <button onClick={() => setOpen(true)}>Edit</button>
      {open && <Modal onClose={() => setOpen(false)}>Settings form…</Modal>}
    </div>
  );
}

The demo below models the core idea — a component renders content but the output is placed in a different container:

// A portal renders JSX into a DIFFERENT DOM node than where the
// component lives. We model it: the component decides the content,
// a "mount" function decides WHERE that content is placed.

function mountInto(container, content) {
  // In React: createPortal(content, container)
  return "<" + container + ">" + content + "</" + container + ">";
}

// Component lives deep in the tree, but its output lands at body level:
const modal = mountInto("body", "🪟 Are you sure?");

console.log("Rendered by a card, but placed at:", modal);
// <body>🪟 Are you sure?</body>
// The card never had to worry about z-index or overflow clipping.

2️⃣ The createPortal Signature

It's a single function with two arguments: what to render and where to render it.

createPortal(children, domNode, key?)
//           ^^^^^^^^  ^^^^^^^
//           the JSX   a real DOM element to mount into

The domNode must already exist in the page. Most apps add a single <div id="modal-root"> to index.html. Your turn — fill in the target container so the tooltip mounts into "tooltip-root":

// 🎯 YOUR TURN — fill in the ___ then press "Try it Yourself".
// A tooltip should be mounted into the "tooltip-root" container.

function mountInto(container, content) {
  return "[" + container + "] " + content;
}

// 1) Mount the tooltip text into the "tooltip-root" container.
const tip = mountInto(___, "Click to copy");   // 👉 use "tooltip-root"

console.log(tip);

// ✅ Expected output:
//    [tooltip-root] Click to copy

3️⃣ Events Bubble Through the React Tree

Here's the detail that trips people up: even though a portal's DOM node lives somewhere else, React events bubble through the React tree — the place where you wrote the portal, not where the DOM ended up. A click inside a portaled modal still reaches an onClick on the component that rendered it.

function Toolbar() {
  // This onClick fires even for clicks inside the portal below,
  // because the portal is a React child of Toolbar.
  return (
    <div onClick={() => console.log("Toolbar heard the click")}>
      <Modal>Click me</Modal>   
    </div>
  );
}

The demo walks the React bubble path so you can see handlers fire up the tree even though the DOM node is elsewhere:

// Surprising but true: a click inside a portal still bubbles to the
// React ANCESTOR that rendered it — not to its DOM neighbor. We model
// the bubble path as the React tree, ignoring where the DOM node sits.

function bubble(reactPath) {
  // Walk up the React tree firing each handler.
  return reactPath.map(node => "fired on <" + node + ">").join("\n");
}

// The button is portaled to <body>, but its React parent is <Toolbar>.
const reactTree = ["button", "Toolbar", "App"];

console.log(bubble(reactTree));
// fired on <button>
// fired on <Toolbar>   <-- handler runs even though DOM is elsewhere!
// fired on <App>
// Events follow the React tree, which is why onClick on Toolbar works.

Reorder Challenge

These lines of a portal-based Tooltip are scrambled. Put them in the correct order:

A.     return createPortal(
B.   function Tooltip({ text }) {
C.       document.getElementById("tooltip-root")
D.       <span className="tooltip">{text}</span>,
E.     );
F.   }

Quick Recall

1) Your modal is being clipped by a parent's overflow: hidden. What's the fix?

Render it through a portal into a node at the end of <body>, so it escapes the clipping ancestor entirely.

2) A button inside a portal is clicked. Will an onClick on the component that rendered the portal fire?

Yes. React events bubble through the React tree, not the DOM tree, so the rendering ancestor still hears the click.

3) What are the two arguments to createPortal?

The JSX to render, and the existing DOM node to render it into: createPortal(children, domNode).

Common Errors (and the fix)

📋 Quick Reference

TaskSyntax
Importimport { createPortal } from "react-dom"
Create a portalcreatePortal(<Modal/>, target)
Get the targetdocument.getElementById("modal-root")
Stop backdrop closeonClick={e => e.stopPropagation()}
Mount point (HTML)<div id="modal-root"></div>

Mini-Challenge: Toast Notification

Toasts almost always live in a portal so they float above everything. Model a toast that mounts a message into the "toast-root" container with a level icon — "success" → "✅".

// A toast mounts a message (with an icon) into a portal container.
function toast(level, message) {
  const icon = level === "success" ? "✅" : "⚠️";
  const container = "toast-root";
  // your code here: return "[" + container + "] " + icon + " " + message
}

console.log(toast("success", "Profile updated"));

// 🎯 Expected:
//    [toast-root] ✅ Profile updated

🎉 Lesson Complete

Practice quiz

Which function creates a React portal?

  • ReactDOM.render
  • usePortal
  • createPortal
  • renderInto

Answer: createPortal. createPortal(children, domNode) renders the children into a different DOM node.

Where is createPortal imported from?

  • react-dom
  • react
  • react-router-dom
  • react/portals

Answer: react-dom. createPortal comes from react-dom, not react.

What are the two main arguments to createPortal?

  • A ref and a callback
  • A component and its props
  • A selector string and an event
  • The JSX/children and the target DOM node

Answer: The JSX/children and the target DOM node. createPortal(children, domNode) takes the JSX to render and an existing DOM node to render it into.

A click inside a portal bubbles through which tree?

  • The DOM tree only
  • The React tree (where the portal was rendered)
  • Neither — events don't bubble from portals
  • The CSS cascade

Answer: The React tree (where the portal was rendered). React events bubble through the React tree, so the rendering ancestor still hears the click even though the DOM node is elsewhere.

What problem do portals primarily solve for modals?

  • They escape parent overflow, z-index, and transform clipping
  • They make state global
  • They prevent re-renders
  • They add accessibility automatically

Answer: They escape parent overflow, z-index, and transform clipping. A portal lifts the DOM output out of clipping ancestors so the overlay can cover the screen.

The target DOM node passed to createPortal must…

  • Be created inside the component's JSX
  • Be a React component
  • Already exist in the page (e.g. a div in index.html)
  • Be the document body only

Answer: Already exist in the page (e.g. a div in index.html). The domNode must already exist; most apps add a <div id="modal-root"> to index.html.

Do Context and state still work inside a portal?

  • No, the portal is isolated from the React tree
  • Yes, the portal is a normal child in the React tree
  • Only state works, not Context
  • Only if you pass them as props

Answer: Yes, the portal is a normal child in the React tree. A portal is a real child in the React tree, so it inherits everything its React ancestors provide.

If getElementById returns null when calling createPortal, the likely cause is…

  • The component is memoized
  • createPortal was imported from react
  • The portal has no key
  • The target div doesn't exist yet in the DOM

Answer: The target div doesn't exist yet in the DOM. Calling getElementById before the target div exists returns null; add it to index.html or create it first.

To stop a backdrop click from closing a modal when clicking inside it, you…

  • Remove the onClick
  • Call e.stopPropagation() on the inner box
  • Use a portal
  • Set z-index to 0

Answer: Call e.stopPropagation() on the inner box. stopPropagation on the inner box prevents the click from reaching the backdrop's close handler.

Which is true about a portaled dialog and accessibility?

  • The portal handles focus trapping for you
  • Portals make dialogs inaccessible
  • You still own focus trap, Escape-to-close, and ARIA roles
  • ARIA roles are added automatically

Answer: You still own focus trap, Escape-to-close, and ARIA roles. The portal only moves the DOM; you still need role="dialog", focus trapping, and Escape-to-close.

Continue this course

Related lessons