React Portals
Use portals to render components outside the DOM hierarchy for modals and overlays.
TL;DR
- 01Use ReactDOM.createPortal to render outside the DOM tree.
- 02Portals are useful for modals, tooltips, and dropdowns.
- 03Event bubbling still works through portals to parent components.
Tips
- 01Use portals for any UI that needs to visually escape its parent container without needing absolute positioning tricks.
Warnings
- 01Event bubbling in portals goes through the React tree, not the DOM tree, so document event listeners won't catch portal events from within React.
Creating Portals
- Use ReactDOM.createPortal to render a component at a different location.
import { createPortal } from "react-dom"; function Modal({ children }) { const root = document.getElementById("modal-root"); return createPortal( <div className="modal">{children}</div>, root ); } - Create a target element in your HTML for the portal.
<div id="root"></div> <div id="modal-root"></div> - Portals render outside the component hierarchy but stay in React.
- Perfect for modals, dropdowns, tooltips, and overlays.
- Check that the target element exists before rendering.
function Portal({ children, containerId = "portal-root" }) { const container = document.getElementById(containerId); if (!container) return null; return createPortal(children, container); }
Modal Implementation
- Build a reusable modal using portals for proper layering.
function Modal({ isOpen, onClose, children, title }) { if (!isOpen) return null; return createPortal( <div className="modal-backdrop" onClick={onClose}> <div className="modal-content" onClick={e => e.stopPropagation()}> <h2>{title}</h2> {children} <button onClick={onClose}>Close</button> </div> </div>, document.getElementById("portal-root") ); } function App() { const [open, setOpen] = useState(false); return ( <> <button onClick={() => setOpen(true)}>Open Modal</button> <Modal isOpen={open} onClose={() => setOpen(false)} title="Hello"> Modal content here </Modal> </> ); } - Use stopPropagation to prevent backdrop clicks from closing the modal.
- Trap keyboard focus inside the modal for accessibility compliance.
useEffect(() => { if (!isOpen) return; const focusable = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); focusable?.[0]?.focus(); }, [isOpen]); - Close the modal when the user presses the Escape key.
useEffect(() => { function handleKey(e) { if (e.key === "Escape") onClose(); } if (isOpen) document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); - Prevent body scroll while the modal is open.
useEffect(() => { document.body.style.overflow = isOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [isOpen]);
Event Bubbling Through Portals
- Events bubble from portals to ancestors in the React component tree.
function Parent() { const handleClick = (e) => { if (e.target.closest(".modal")) { console.log("Modal clicked"); } }; return ( <div onClick={handleClick}> <Modal>Content</Modal> </div> ); } - Event bubbling goes through the React tree, not the DOM tree.
- This allows parent components to handle events from portaled children.
- Useful for closing modals when clicking outside.
- Use stopPropagation inside the portal to prevent bubbling to parent handlers.
function Modal({ onClose, children }) { return createPortal( <div className="backdrop" onClick={onClose}> <div className="content" onClick={e => e.stopPropagation()}> {children} </div> </div>, document.getElementById("modal-root") ); }
Common Portal Use Cases
- Tooltips that overflow their containers and need body-level placement.
function Tooltip({ content, children }) { return ( <> {children} {createPortal( <div className="tooltip">{content}</div>, document.body )} </> ); } - Dropdowns and autocomplete menus that escape overflow-hidden parents.
function Dropdown({ isOpen, options }) { if (!isOpen) return null; return createPortal( <ul className="dropdown"> {options.map(opt => <li key={opt}>{opt}</li>)} </ul>, document.body ); } - Notifications and toasts floating above all other content.
function Toast({ message }) { return createPortal( <div className="toast">{message}</div>, document.getElementById("toast-container") ); } - Context menus that must appear at the cursor position on the page.
function ContextMenu({ x, y, items, onClose }) { return createPortal( <ul className="context-menu" style={{ top: y, left: x }}> {items.map(item => ( <li key={item.label} onClick={() => { item.action(); onClose(); }}> {item.label} </li> ))} </ul>, document.body ); } - Lightboxes that render full-screen images above all page content.
function Lightbox({ src, alt, onClose }) { return createPortal( <div className="lightbox" onClick={onClose}> <img src={src} alt={alt} /> </div>, document.getElementById("portal-root") ); }
Portal Best Practices
- Always have a target element in your HTML.
<body> <div id="root"></div> <div id="modal-root"></div> <div id="tooltip-root"></div> </body> - Clean up portals when components unmount.
useEffect(() => { return () => { // Cleanup if needed }; }, []); - Use z-index in CSS to layer portaled elements correctly.
.modal-backdrop { z-index: 1000; } - Manage focus when opening a modal for keyboard accessibility.
useEffect(() => { if (isOpen) { closeButtonRef.current?.focus(); } }, [isOpen]); - Add role="dialog" and aria-modal="true" for screen reader support.
createPortal( <div role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 id="modal-title">Confirm Delete</h2> {children} </div>, document.getElementById("modal-root") )
FAQ
Use ReactDOM.createPortal(element, document.body) to mount the modal directly on the body while keeping it inside your React tree. This means context providers and state from parent components remain accessible inside the portal.
Portal events bubble through the React component tree, not the DOM tree, so native document-level listeners won't fire for clicks inside a portal. Move event handling into React components using onClick or similar synthetic event handlers instead.
Portals are preferable when a parent component has overflow: hidden, transform, or z-index stacking context issues that would clip or obscure your overlay even with fixed positioning. Use position: fixed inside a portal for the cleanest combination of both.
Yes — portal children still belong to the React tree where createPortal is called, so Context, Redux state, and hooks all work normally even though the portal renders to a different DOM node.
createPortal keeps the component inside React's reconciliation cycle, giving you lifecycle methods, hooks, and automatic cleanup on unmount. Raw DOM manipulation bypasses React entirely, requiring manual cleanup and losing all React features.