How do React Portals work?
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.
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.
// ❌ 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
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.
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>
);
}
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.
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.
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
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.
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>
);
}
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
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