React Portals let you render a component’s children into a DOM node that exists outside of the normal React component hierarchy.
Let’s see it in action. Imagine you want a modal that’s always a direct child of <body> to avoid z-index issues or to escape the constraints of a parent component’s CSS.
Here’s a simple modal component:
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children }) {
// Create a div element that will be appended to the document body.
const modalRoot = document.getElementById('modal-root');
if (!modalRoot) {
// This is a fallback for development. In production, ensure 'modal-root' exists.
const rootDiv = document.createElement('div');
rootDiv.id = 'modal-root';
document.body.appendChild(rootDiv);
return ReactDOM.createPortal(children, rootDiv);
}
return ReactDOM.createPortal(children, modalRoot);
}
// In your index.html, you'd have:
// <div id="app-root"></div>
// <div id="modal-root"></div> // This is crucial!
// And in your App.js:
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
{showModal && (
<Modal>
<div className="modal-content">
<h2>This is a Modal</h2>
<p>It's rendered outside the main app tree!</p>
<button onClick={() => setShowModal(false)}>Close</button>
</div>
</Modal>
)}
</div>
);
}
export default App;
The ReactDOM.createPortal(child, container) function is the core. child is what you want to render (your JSX), and container is the DOM node you want to render it into. In our example, modalRoot is the DOM node, which we explicitly fetch or create to be a direct child of document.body.
This solves a common problem: when you need to break out of an element with overflow: hidden or position: relative that’s constraining a child component. Think tooltips, dropdowns, or modals. Without portals, these elements can get clipped or their positioning can become a nightmare due to parent CSS. By rendering them directly into <body>, they exist in their own isolated DOM subtree, free from the styling constraints of their React parent.
Crucially, even though the DOM node is outside the React component tree, event propagation still works as if the portal were a regular child. If you click inside the modal, the event will bubble up through the React component tree. This means you don’t lose event handling capabilities.
The container argument for createPortal can be any DOM element. It doesn’t have to be a direct child of <body>. You could, for instance, create a specific div within your App component’s JSX and then use that div as the portal’s container if you wanted to render something outside its immediate parent’s DOM constraints but still within a specific section of your app’s structure.
When you’re debugging, remember that the DOM element you’re rendering into must exist before createPortal is called. If your container element isn’t found, createPortal will return null and nothing will render. This is why the example includes a check and fallback creation for modal-root in development, but in production, it’s best practice to ensure the target DOM node is present in your index.html.
The most surprising thing about portals is how seamlessly they integrate with React’s lifecycles and event system. A component rendered via a portal still receives context, its state updates correctly, and its lifecycle methods (like componentDidMount or useEffect) fire as expected. The React tree’s logical structure is maintained, even when the DOM is physically separated.
The next hurdle is often managing focus when using portals for interactive elements like modals.