ReactMedium

How do React Portals work?

01

The Short Answer

React Portals let you render a component's children into a DOM node that exists outside the parent component's DOM hierarchy — while keeping the React event bubbling and context intact. You create a portal with createPortal(children, domNode). The most common use case is modals, tooltips, and dropdowns that need to visually 'break out' of a parent with overflow: hidden or a restrictive z-index stacking context, but still behave as part of the React component tree for events and state.

02

The Problem Portals Solve

Normally, when you render a child component, it becomes a DOM child of its parent's DOM node. This is usually fine, but it causes problems when a child needs to visually appear above or outside its parent. If a parent has overflow: hidden, a dropdown inside it gets clipped. If a parent has a low z-index, a modal inside it can't appear above other elements. Portals solve this by rendering the DOM nodes elsewhere (typically directly on document.body) while keeping the React tree structure unchanged.

overflow-problem.tsxtsx
// ❌ Problem: dropdown gets clipped by parent's overflow: hidden
function Card() {
  return (
    <div className="overflow-hidden rounded-lg"> {/* clips children */}
      <h2>Card Title</h2>
      <Dropdown>
        {/* This dropdown can't extend beyond the card boundaries */}
        {/* It gets cut off by overflow: hidden */}
        <DropdownItem>Option 1</DropdownItem>
        <DropdownItem>Option 2</DropdownItem>
      </Dropdown>
    </div>
  );
}

// ✅ Solution: portal renders dropdown outside the overflow container
// The dropdown DOM node lives on document.body
// But in React's tree, it's still a child of Card
03

Creating a Portal

You use createPortal from react-dom. It takes two arguments: the JSX to render, and the DOM node to render it into. The portal's children will appear in the target DOM node, but from React's perspective they're still children of the component that renders the portal — meaning context, event bubbling, and state all work as if the portal were rendered in place.

basic-portal.tsxtsx
import { createPortal } from 'react-dom';
import { useState } from 'react';

function Modal({ isOpen, onClose, children }: {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}) {
  if (!isOpen) return null;

  // Renders into document.body, not into the parent's DOM node
  return createPortal(
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/50"
        onClick={onClose}
        aria-hidden="true"
      />
      {/* Modal content */}
      <div
        role="dialog"
        aria-modal="true"
        className="relative z-10 rounded-lg bg-background p-6 shadow-xl"
      >
        {children}
      </div>
    </div>,
    document.body // Target DOM node
  );
}

// Usage — Modal renders into body, but React treats it as a child of App
function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div className="overflow-hidden"> {/* overflow won't clip the modal */}
      <button onClick={() => setShowModal(true)}>Open Modal</button>
      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Title</h2>
        <p>This renders on document.body but is still part of App's React tree</p>
      </Modal>
    </div>
  );
}
04

Event Bubbling Through Portals

The most important thing to understand about portals is that events bubble through the React tree, not the DOM tree. Even though the portal's DOM nodes are on document.body, a click inside the portal will bubble up through the React component hierarchy — reaching the parent that rendered the portal. This is powerful but can be surprising if you're not expecting it.

event-bubbling.tsxtsx
import { createPortal } from 'react-dom';

function Parent() {
  // This onClick catches clicks from INSIDE the portal!
  // Even though the portal's DOM is on document.body
  const handleClick = () => console.log('Parent caught click');

  return (
    <div onClick={handleClick}>
      <h1>Parent Component</h1>
      {createPortal(
        <button onClick={() => console.log('Portal button clicked')}>
          Click me (I'm in a portal)
        </button>,
        document.body
      )}
    </div>
  );
}

// Clicking the portal button logs:
// 1. "Portal button clicked" (direct handler)
// 2. "Parent caught click" (bubbled through React tree)

// In the DOM tree, the button is a child of <body>
// In the React tree, the button is a child of the <div onClick={handleClick}>
// React's synthetic events follow the React tree, not the DOM tree

This event bubbling behavior means portals work seamlessly with context providers, error boundaries, and event delegation patterns. A modal rendered via portal still has access to its parent's context and its errors still get caught by parent error boundaries.

05

Common Use Cases

Portals are the right tool whenever a component needs to visually escape its parent's DOM constraints while remaining logically connected to it in the React tree.

  • Modals and dialogs — need to appear above everything, regardless of parent z-index
  • Tooltips and popovers — must not be clipped by overflow: hidden containers
  • Dropdown menus — need to extend beyond scrollable containers
  • Toast notifications — render at a fixed position on the page
  • Full-screen overlays — loading screens, image lightboxes
06

Portal with a Dedicated Container

Instead of rendering directly into document.body, it's often cleaner to create a dedicated container element for your portals. This keeps portal content organized, makes it easier to apply global styles or animations, and avoids potential conflicts with other libraries that also append to body.

portal-container.tsxtsx
import { createPortal } from 'react-dom';
import { useEffect, useRef, useState } from 'react';

// Reusable Portal component that creates its own container
function Portal({ children, containerId = 'portal-root' }: {
  children: React.ReactNode;
  containerId?: string;
}) {
  const [mounted, setMounted] = useState(false);
  const containerRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    // Find or create the portal container
    let container = document.getElementById(containerId);
    if (!container) {
      container = document.createElement('div');
      container.id = containerId;
      document.body.appendChild(container);
    }
    containerRef.current = container;
    setMounted(true);

    return () => {
      // Clean up if container is empty
      if (container && container.childNodes.length === 0) {
        container.remove();
      }
    };
  }, [containerId]);

  if (!mounted || !containerRef.current) return null;
  return createPortal(children, containerRef.current);
}

// Usage
function Tooltip({ text, position }: { text: string; position: { x: number; y: number } }) {
  return (
    <Portal containerId="tooltip-root">
      <div
        className="fixed rounded bg-foreground px-2 py-1 text-sm text-background"
        style={{ left: position.x, top: position.y }}
      >
        {text}
      </div>
    </Portal>
  );
}
07

Accessibility Considerations

Portals move DOM nodes outside their logical parent, which can confuse screen readers if not handled properly. For modals, you need to manage focus trapping (keep focus inside the modal), set aria-modal="true", and use aria-hidden="true" on the rest of the page content. When the modal closes, focus should return to the element that triggered it.

Accessibility requirements for portal-based modals

  • Trap focus inside the portal (Tab should cycle within the modal)
  • Set role="dialog" and aria-modal="true" on the modal container
  • Apply aria-hidden="true" to the main content behind the modal
  • Return focus to the trigger element when the modal closes
  • Close on Escape key press
08

Why Interviewers Ask This

Portal questions test whether you understand the distinction between the DOM tree and the React component tree. Interviewers want to see that you know when and why to use portals (overflow/z-index escape hatches), understand that events bubble through the React tree not the DOM tree, can explain how context and error boundaries still work across portals, know the accessibility implications of moving DOM nodes, and have practical experience building modals or tooltips. It shows you've dealt with real layout challenges beyond basic component composition.

Quick Revision Cheat Sheet

API: createPortal(children, domNode) from 'react-dom'

What it does: Renders children into a different DOM node while keeping React tree intact

Event bubbling: Events bubble through React tree (parent components), not DOM tree

Context/errors: Context providers and error boundaries work across portals normally

Use cases: Modals, tooltips, dropdowns, toasts — anything escaping overflow/z-index

A11y: Must manage focus trapping, aria-modal, and focus restoration manually