React Portals
Rendering components outside the DOM hierarchy
🚪 What are React Portals?
React Portals let you render components outside their parent DOM hierarchy while maintaining React's event system. They're perfect for modals, tooltips, and overlays that need to escape CSS overflow or z-index constraints.
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(
children,
document.getElementById('modal-root')
);
}
Key Portal Concepts
createPortal
Render to a different DOM node
createPortal(
child,
container
)
DOM Escape
Break out of parent constraints
// Renders outside parent
document.body
Event Bubbling
Events still bubble through React tree
// Parent can catch events
onClick={handleClick}
Use Cases
Modals, tooltips, and overlays
// Common patterns
Modal, Tooltip, Dropdown
🔹 Setting Up Portal Root
Before using portals, create a dedicated DOM node in your HTML where portal content will render. This container sits outside your React root, allowing components to escape their parent hierarchy cleanly.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React App</title>
</head>
<body>
<!-- Main React app -->
<div id="root"></div>
<!-- Portal container -->
<div id="modal-root"></div>
<div id="tooltip-root"></div>
</body>
</html>
Note: Portal containers should be siblings to your main React root, not children.
🔹 Basic Portal Example
Creating a basic portal involves using createPortal to render content into a different DOM node. The component appears in the specified container while maintaining its place in the React component tree for state and props.
// Portal.jsx
import { createPortal } from 'react-dom';
function Portal({ children }) {
const portalRoot = document.getElementById('modal-root');
return createPortal(
children,
portalRoot
);
}
export default Portal;
// App.jsx
import { useState } from 'react';
import Portal from './Portal';
function App() {
const [showPortal, setShowPortal] = useState(false);
return (
<div>
<h1>My App</h1>
<button onClick={() => setShowPortal(!showPortal)}>
Toggle Portal
</button>
{showPortal && (
<Portal>
<div style={{ background: 'lightblue', padding: '20px' }}>
This renders in #modal-root!
</div>
</Portal>
)}
</div>
);
}
🔹 Modal with Portal
Modals are the most common use case for portals. They render above all other content with an overlay, escaping parent container constraints like overflow hidden or z-index stacking, ensuring proper display regardless of parent styles.
// Modal.jsx
import { createPortal } from 'react-dom';
import './Modal.css';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>
×
</button>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
export default Modal;
/* Modal.css */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 500px;
width: 90%;
position: relative;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 30px;
cursor: pointer;
color: #999;
}
// Usage
import { useState } from 'react';
import Modal from './Modal';
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>
Open Modal
</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<h2>Modal Title</h2>
<p>This modal uses a portal!</p>
</Modal>
</div>
);
}
🔹 Tooltip with Portal
Tooltips benefit from portals by rendering above all content without z-index conflicts. Portals ensure tooltips display correctly even when their trigger element is inside containers with overflow hidden or complex positioning.
// Tooltip.jsx
import { useState } from 'react';
import { createPortal } from 'react-dom';
import './Tooltip.css';
function Tooltip({ children, text }) {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseEnter = (e) => {
const rect = e.target.getBoundingClientRect();
setPosition({
x: rect.left + rect.width / 2,
y: rect.top - 10
});
setShow(true);
};
return (
<>
<span
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setShow(false)}
>
{children}
</span>
{show && createPortal(
<div
className="tooltip"
style={{
left: `${position.x}px`,
top: `${position.y}px`
}}
>
{text}
</div>,
document.getElementById('tooltip-root')
)}
</>
);
}
export default Tooltip;
/* Tooltip.css */
.tooltip {
position: fixed;
transform: translate(-50%, -100%);
background: #333;
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
pointer-events: none;
z-index: 9999;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
}
// Usage
import Tooltip from './Tooltip';
function App() {
return (
<div>
<p>
Hover over <Tooltip text="This is a tooltip!">
<strong>this text</strong>
</Tooltip> to see the tooltip.
</p>
</div>
);
}
🔹 Event Bubbling in Portals
Despite rendering in a different DOM location, portal events bubble through the React component tree. This means parent components can catch events from portal children, maintaining React's synthetic event system and component communication patterns.
// EventBubbling.jsx
import { createPortal } from 'react-dom';
function PortalChild() {
return createPortal(
<button>Click me (in portal)</button>,
document.getElementById('modal-root')
);
}
function App() {
const handleClick = () => {
alert('Event bubbled from portal!');
};
return (
<div onClick={handleClick}>
<h1>Parent Component</h1>
<p>Click the button below:</p>
{/* Button renders in portal but event bubbles to parent */}
<PortalChild />
</div>
);
}
export default App;
Important: Events bubble through the React tree, not the DOM tree. This is a key feature of portals.
🔹 When to Use Portals
Portals solve specific rendering challenges where components need to escape their parent's visual constraints. Understanding when to use portals helps create better user interfaces with proper layering and positioning.
✅ Good Use Cases:
- Modals: Full-screen overlays that need to appear above everything
- Tooltips: Small popups that shouldn't be clipped by parent overflow
- Dropdowns: Menus that need to escape scrollable containers
- Notifications: Toast messages that appear in a fixed position
- Popovers: Context menus and floating panels
❌ Avoid Portals For:
- Regular component rendering within normal flow
- Simple conditional rendering
- Components that don't need to escape parent constraints