Technology · React
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.
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") )
Tip: Use portals for any UI that needs to visually escape its parent container without needing absolute positioning tricks.
Warning: Event bubbling in portals goes through the React tree, not the DOM tree, so document event listeners won't catch portal events from within React.