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.
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.
JSXimport { 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:
- Capture Focus: On mount, move focus to the first interactive element.
- Intercept Tab: Add a listener for the
Tabkey. - 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
- Create a
Tooltipcomponent that uses thePortalpattern above. - Add a
refto a button that triggers the tooltip. - On mouse enter, calculate the trigger's
getBoundingClientRectand update the tooltip's absolute position relative to the viewport. - 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.bodyis the only place to mount. Sometimes, you need multiple portals (e.g., one for global notifications, one for modals). Create a dedicatedPortalProviderto manage these mount points. - SSR Hydration: If you are using Server-Side Rendering (SSR),
documentis not available during the initial render. Ensure youruseEffectoruseLayoutEffecthandles 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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.