Back to Blog
Lesson 38 of the Advanced React: Performance, Architecture & Patterns course
ReactJune 28, 20264 min read

Using Portals for UI Overlays: A Senior Engineer’s Guide

Learn to use React Portals to break out of the DOM hierarchy for modals, tooltips, and overlays while maintaining full state and accessibility control.

ReactPortalsDOMUIAccessibilityModalsjavascriptfrontend

Previously in this course, we explored Managing Global Modals: A Scalable Pattern for React UI, which taught us how to handle modal visibility without prop drilling. While that lesson focused on state architecture, this lesson focuses on the DOM rendering strategy: Portals.

Portals provide a first-class way to render children into a DOM node that exists outside the hierarchy of the parent component. This is critical for UI overlays like modals, dropdowns, and tooltips where CSS properties like overflow: hidden or z-index on a parent container would otherwise clip or obscure your UI.

The Mechanics of React Portals

At its core, a Portal is a way to break the physical DOM structure while keeping the React logical tree intact. When you render a component via createPortal, React keeps the component within the same context and lifecycle as its parent, but the actual DOM elements are moved to a different location in the document body.

This is essential because of how the browser's box model works. If you have a modal defined inside a nested div with position: relative and z-index: 1, that modal is trapped by its parent's stacking context. By using Portals, we can inject that modal directly into document.body, effectively "escaping" the parent's constraints.

Implementing a Portal Component

To use a portal, we need a target DOM node. We typically create an "overlay root" in our index.html or inject it dynamically on mount.

JSX
import { useEffect, useState } from CE9178">'react';
import { createPortal } from CE9178">'react-dom';

const Portal = ({ children }) => {
  const [container] = useState(() => document.createElement(CE9178">'div'));

  useEffect(() => {
    document.body.appendChild(container);
    return () => document.body.removeChild(container);
  }, [container]);

  return createPortal(children, container);
};

This implementation ensures that the container is appended to the body when the component mounts and cleaned up when it unmounts. Because we use useState to initialize the container, the node is stable across re-renders.

Managing Focus Traps

One common pitfall when moving elements outside the DOM hierarchy is that keyboard navigation can break. If a user tabs through your application, they expect the focus to stay within the modal. Since the modal is now at the end of the document.body, the browser's native tab flow might jump back to the browser address bar rather than the next element in the modal.

To solve this, you need a focus trap. While libraries like react-focus-lock are industry standards, understanding the principle is key:

  1. Capture Focus: On mount, move focus to the first interactive element.
  2. Intercept Tab: Add a listener for the Tab key.
  3. Boundary Check: If the user is on the last element and hits Tab, shift focus back to the first element.

Handling Overlay Positioning

For tooltips or popovers, positioning is rarely static. You often need to calculate the coordinates of a trigger element and position the portal accordingly.

Instead of hardcoding CSS, use the getBoundingClientRect() method on the trigger element. You can then pass these coordinates to your Portal component to apply inline styles or use a library like Floating UI (the evolution of Popper.js) to handle edge cases like screen-edge flipping.

Hands-on Exercise

  1. Create a Tooltip component that uses the Portal pattern above.
  2. Add a ref to a button that triggers the tooltip.
  3. On mouse enter, calculate the trigger's getBoundingClientRect and update the tooltip's absolute position relative to the viewport.
  4. Ensure that the tooltip remains visible even if the trigger is inside a container with overflow: hidden.

Common Pitfalls

  • Event Bubbling: Even though a portal is physically moved in the DOM, it still behaves like a child in the React tree. Events will bubble up to the parent component in the React hierarchy, not the DOM hierarchy. See Mastering Event Bubbling and Propagation in React for details on why this matters.
  • Missing Root Nodes: Don't assume document.body is the only place to mount. Sometimes, you need multiple portals (e.g., one for global notifications, one for modals). Create a dedicated PortalProvider to manage these mount points.
  • SSR Hydration: If you are using Server-Side Rendering (SSR), document is not available during the initial render. Ensure your useEffect or useLayoutEffect handles the creation of the portal container only after the component mounts on the client.

Recap

Portals allow us to transcend the DOM hierarchy without losing the benefits of React’s component model. By carefully managing our mount nodes and implementing robust focus trapping, we can build complex, accessible UI overlays that aren't hampered by the constraints of parent CSS. Remember that while Portals solve the physical placement problem, they don't change how React handles state or events, making them a safe and powerful tool for building high-performance UI components.

Up next: We will dive into Implementing Virtualized Lists to handle rendering massive datasets efficiently.

Similar Posts