React Portals
What are Portals?
Portals let you render a component's output into a different DOM node than its parent. This is essential for modals, tooltips, and dropdowns that need to visually "escape" their parent's CSS constraints (like overflow: hidden or z-index stacking contexts).
Even though the portal renders outside the parent DOM node, it still behaves like a normal React child — events bubble up through the React tree, not the DOM tree.
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
// Modal component — renders into #modal-root, not inside parent
function Modal({ isOpen, onClose, title, children }) {
// Close on Escape key
useEffect(() => {
if (!isOpen) return
const handleEsc = (e) => { if (e.key === 'Escape') onClose() }
document.addEventListener('keydown', handleEsc)
// Prevent body scroll when modal is open
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', handleEsc)
document.body.style.overflow = ''
}
}, [isOpen, onClose])
if (!isOpen) return null
// createPortal(children, domNode)
return createPortal(
<div
className="modal-overlay"
onClick={onClose}
style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000
}}
>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
style={{
background: 'white', borderRadius: '8px',
padding: '24px', maxWidth: '500px', width: '90%'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h2>{title}</h2>
<button onClick={onClose}>×</button>
</div>
{children}
</div>
</div>,
document.getElementById('modal-root') // render here, not in parent
)
}
// Usage
function App() {
const [isOpen, setIsOpen] = useState(false)
return (
<div style={{ overflow: 'hidden', height: '200px' }}>
{/* Even with overflow:hidden on parent, modal renders correctly */}
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="Confirm Action">
<p>Are you sure you want to delete this item?</p>
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={() => { alert('Deleted!'); setIsOpen(false) }}>Delete</button>
</Modal>
</div>
)
}
import { createPortal } from 'react-dom'
import { useState, useRef, useEffect } from 'react'
// Tooltip using portal — avoids z-index/overflow issues
function Tooltip({ children, text }) {
const [visible, setVisible] = useState(false)
const [position, setPosition] = useState({ top: 0, left: 0 })
const triggerRef = useRef(null)
const showTooltip = () => {
const rect = triggerRef.current.getBoundingClientRect()
setPosition({
top: rect.top - 40 + window.scrollY,
left: rect.left + rect.width / 2 + window.scrollX,
})
setVisible(true)
}
return (
<>
<span
ref={triggerRef}
onMouseEnter={showTooltip}
onMouseLeave={() => setVisible(false)}
>
{children}
</span>
{visible && createPortal(
<div style={{
position: 'absolute',
top: position.top,
left: position.left,
transform: 'translateX(-50%)',
background: '#333', color: 'white',
padding: '4px 8px', borderRadius: '4px',
fontSize: '12px', whiteSpace: 'nowrap',
zIndex: 9999, pointerEvents: 'none',
}}>
{text}
</div>,
document.body
)}
</>
)
}
// Toast notification system using portal
function ToastContainer({ toasts, removeToast }) {
return createPortal(
<div style={{ position: 'fixed', bottom: '20px', right: '20px', zIndex: 9999 }}>
{toasts.map(toast => (
<div key={toast.id} className={`toast toast-${toast.type}`}>
{toast.message}
<button onClick={() => removeToast(toast.id)}>×</button>
</div>
))}
</div>,
document.body
)
}
<!-- index.html — add portal mount points -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React App</title>
</head>
<body>
<!-- Main React app root -->
<div id="root"></div>
<!-- Portal mount points -->
<div id="modal-root"></div>
<div id="tooltip-root"></div>
<div id="toast-root"></div>
<!-- Portals render here, outside #root -->
<!-- But React events still bubble through the React tree -->
</body>
</html>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.