Skip to content
C Codeloom
React

React Portals and Modals: A Practical Tutorial

Learn how React portals work, when to use them, and how to build accessible modals that escape parent overflow and z-index traps.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What a React portal is and how it changes the DOM tree
  • Why modals so often need portals
  • How event bubbling still works through portals
  • How to add focus trapping and ESC handling
  • Pitfalls with SSR and hydration

Prerequisites

  • Comfortable with JS and HTML
  • Basic React component knowledge

What and Why

A React portal renders a child into a DOM node that lives outside the parent component’s DOM hierarchy. The React tree and event system stay the same, but the actual HTML output moves elsewhere, usually to a sibling of body.

Why bother? Modals, tooltips, popovers, and toasts all suffer from the same problem: they need to visually break out of the page layout. A modal trapped inside a parent with overflow: hidden will be clipped. A tooltip inside a card with transform will inherit a new stacking context and might get covered. Portals solve these problems by escaping the offending parent entirely.

Mental Model

Imagine React maintains two parallel realities. One is the component tree, the parent-child relationships your code expresses. The other is the DOM tree, what the browser actually paints. Most of the time these match. A portal lets them disagree on purpose.

The component still receives props from its parent. Events still bubble up to the parent’s handlers. Context still flows down. But the rendered HTML lives somewhere else. To the browser, the modal is a child of body. To React, it is still a child of <Page>.

React tree:        DOM tree:
App                body
 |                  |-- div#root
 Page               |    |-- App markup
  |                 |-- div#portal-root
  Modal*                  |-- Modal markup
                          
* Modal portals out, but events bubble to Page in React land.
React tree vs DOM tree with a portal

Hands-on Example

First, add a target div in your HTML, often in your root layout:

<div id="portal-root"></div>

Then a minimal modal component:

import { useEffect } from 'react';
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  useEffect(() => {
    function onKey(e) {
      if (e.key === 'Escape') onClose();
    }
    if (open) window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose]);

  if (!open) return null;
  const target = document.getElementById('portal-root');

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    target
  );
}

This portal sits visually on top of everything, ignores parent overflow, listens for ESC, and closes when you click the backdrop. Note that even though the modal is a sibling of body in the DOM, the React onClick still bubbles up to whatever parent component rendered the <Modal>.

For accessibility, you also need focus trapping: when the modal opens, focus moves inside, and tabbing wraps. Libraries like @radix-ui/react-dialog and react-aria handle this for you and are worth using in production.

Common Pitfalls

The biggest gotcha is server-side rendering. document does not exist on the server, so getElementById blows up. Wrap your portal logic in a useEffect or render null until mounted. Frameworks like Next.js and Astro often add a tiny useIsMounted hook for this exact reason.

Another trap is forgetting to stop event propagation. If your backdrop closes the modal on click and you do not stop propagation on the inner card, clicking inside the modal also closes it. Use stopPropagation or check event.target === event.currentTarget.

People also confuse the React tree with the DOM tree. CSS selectors like .parent .child no longer match because the child has moved. If your modal’s styles break after portaling, the problem is usually inherited CSS being lost.

Finally, never forget aria-modal, a focus trap, and returning focus to the trigger on close. A portal alone does not give you an accessible modal.

Best Practices

  • Create a single portal root in your root layout, not one per modal.
  • Lock body scroll while the modal is open by adding overflow: hidden to html.
  • Use a well-tested dialog library when shipping to users with disabilities.
  • Make sure the modal’s z-index is high but bounded; do not race to 99999.
  • Animate enter and exit on the inner card, not the portal target itself.

Wrap-up

Portals are the cleanest answer to the question, “how do I escape my parent’s box?” They keep your component tree tidy while giving you full visual freedom. Combined with proper accessibility, they let you build modals, popovers, and toasts that behave correctly across every browser. Master them and you will never fight overflow: hidden again.