Portals let you render part of a React component into a different place in the DOM. Normally, a component renders inside its parent element. A portal allows that same component to stay in the React tree while its HTML is inserted somewhere else, such as directly inside document.body or a separate DOM node like #modal-root.
This is useful when a UI element needs to visually escape its container. For example, a modal should appear above the whole screen, a tooltip should float over other elements, and a dropdown should not be cut off by a parent with overflow: hidden.
Even though the portal content is rendered elsewhere in the DOM, React still treats it as part of the same component tree. That means props, state, context, and event bubbling still work the React way.
z-index and overflow layout problems cleanlyReact provides portals through createPortal from react-dom. It takes two main values:
import { createPortal } from 'react-dom'
function Example() {
return createPortal(
<div>This content is rendered somewhere else in the DOM.</div>,
document.getElementById('portal-root')
)
}
The component is still written and managed in the same React app, but its output is mounted into a different DOM node.
Many React projects add one or more extra mount points in index.html. A common pattern is to keep the main app inside #root and reserve another element such as #modal-root for portal content.
<!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>
<div id="root"></div>
<div id="modal-root"></div>
</body>
</html>
A modal is the most common portal example. If you render a modal inside a deeply nested component, it may be affected by the parent layout. Rendering it through a portal avoids those problems and lets the modal sit above the whole page.
import { createPortal } from 'react-dom'
import { useEffect } from 'react'
function Modal({ isOpen, onClose, title, children }) {
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose()
}
}
document.addEventListener('keydown', handleKeyDown)
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.body.style.overflow = ''
}
}, [isOpen, onClose])
if (!isOpen) return null
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-box" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose}>Close</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.getElementById('modal-root')
)
}
export default Modal
import { useState } from 'react'
import Modal from './Modal'
function App() {
const [open, setOpen] = useState(false)
return (
<div style={{ overflow: 'hidden', height: '220px' }}>
<h2>Portal Example</h2>
<p>This container has limited space, but the modal will still appear correctly.</p>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal isOpen={open} onClose={() => setOpen(false)} title="Delete Item">
<p>Are you sure you want to delete this item?</p>
<button onClick={() => setOpen(false)}>Cancel</button>
<button onClick={() => setOpen(false)}>Delete</button>
</Modal>
</div>
)
}
Tooltips are another great use case for portals. A tooltip often needs to appear above nearby content and ignore clipping from parent elements. With a portal, the tooltip can be positioned visually where it belongs without being trapped by the local layout.
import { createPortal } from 'react-dom'
import { useRef, useState } from 'react'
function Tooltip({ text, children }) {
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 + window.scrollY - 36,
left: rect.left + window.scrollX + rect.width / 2
})
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: '#222',
color: '#fff',
padding: '6px 10px',
borderRadius: '4px',
fontSize: '12px',
whiteSpace: 'nowrap',
zIndex: 9999
}}
>
{text}
</div>,
document.body
)}
</>
)
}
One thing that surprises beginners is that a portal changes where content appears in the DOM, but it does not remove it from the React tree. That leads to several important behaviors:
For example, if a button inside a portal is clicked, React event handling can still reach a parent component higher in the React tree even though the DOM nodes are separated.
modal-rootindex.htmlReact portals solve a visual placement problem. They let you keep a component in the same React hierarchy while rendering its HTML somewhere else in the DOM. This is especially useful for UI elements that must appear above the rest of the page or escape container layout restrictions.
Once you understand createPortal, you can build modals, tooltips, overlays, and other floating UI in a cleaner and more reliable way.
Explore 500+ free tutorials across 20+ languages and frameworks.