Suggest an editImprove this articleRefine the answer for “What is portal in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**React Portal** renders children into a DOM node outside the parent component's hierarchy while keeping them in React's component tree. ```jsx import { createPortal } from 'react-dom'; function Modal({ children }) { return createPortal( <div className="modal">{children}</div>, document.body // renders outside parent DOM ); } ``` **Key point:** the child escapes parent CSS (overflow, z-index, transform) but keeps full access to React state, context, and events.Shown above the full answer for quick recall.Answer (EN)Image**React Portal** renders children into a DOM node outside the parent component's hierarchy while keeping them connected to React's virtual DOM tree, state, and event system. ## Theory ### TL;DR - Portal is like mailing a letter: your JSX gets delivered to a different DOM address, but stays connected to the sender for updates and events. - Main difference: it bypasses parent CSS (z-index, overflow:hidden) without losing React state or context. - React keeps the fiber node under the calling component, so hooks, effects, and context flow normally. - Use when a child needs to escape CSS clipping or stacking. Skip for normal nesting. ### Quick example ```jsx import { createPortal } from 'react-dom'; import { useState } from 'react'; function DropdownMenu() { const [open, setOpen] = useState(false); return ( <> <button onClick={() => setOpen(true)}>Menu</button> {open && createPortal( <div style={{ position: 'absolute', background: 'white', border: '1px solid #ccc' }}> <button onClick={() => setOpen(false)}>Close</button> </div>, document.getElementById('dropdown-root') // renders outside #root )} </> ); } ``` The dropdown appears above the clipped parent, state stays synced, and click events still bubble through the React tree. ### Key difference from regular rendering Portals detach only the final DOM output from the parent subtree. React still keeps the fiber node under the calling component, so state, effects, and [context providers](/questions/what-is-context-in-react) all work as expected. The only thing that changes is where the final paint happens in the DOM. This is why a modal inside a `transform: rotateX(10deg)` container gets displaced or clipped. The CSS stacking context traps it. A portal escapes that trap entirely. ### When to use - Modal over a parent with `z-index: 10` - portal to `body`, render at `z-index: 9999`. - Tooltip inside a CSS-transformed container - portal escapes transform origin limits. - Dropdown in an `overflow: hidden` table cell - portal to `document.body` avoids clipping. - Notification stack at screen edge - portal keeps it independent from layout flow. Skip portals when the child fits within parent styles or needs relative positioning to its parent. ### How it works internally React's reconciler creates a fiber for the portal child under the calling component but sets its `containerInfo` to the target DOM node. During the commit phase, DOM mutations go to that target via `container.appendChild`. Events still proxy through React's synthetic event system regardless of DOM location, so `onClick` bubbles through the React tree, not the DOM tree. The SSR mismatch tends to trip up teams more than the z-index problem. The z-index issue is visible immediately. The hydration error shows up later, in production. ### Common mistakes **Portaling to a node that doesn't exist:** ```jsx createPortal(<div>Hi</div>, document.getElementById('missing')); // null - throws in React 18 ``` Fix: create the element inside a [useEffect](/questions/what-is-use-effect-in-react) and append it to `document.body`. **Forgetting that DOM parent differs from React parent:** ```jsx document.getElementById('modal-root').children[0].parentNode === document.getElementById('app-root'); // false ``` DOM hierarchy and React hierarchy are separate. Traversing the DOM won't reflect the component tree structure. **SSR hydration mismatch:** ```jsx // Server has no #portal-root, client tries to portal - React 18 throws createPortal(<div>content</div>, document.getElementById('portal-root')); ``` Fix: guard with a `mounted` state set inside `useEffect`. Return a hidden placeholder server-side. **No cleanup on unmount:** If you manually create a container element in `useEffect`, remove it in the cleanup function. Otherwise DOM nodes pile up across re-mounts. ### Real-world usage - **Material-UI (v5)** - modals and dropdowns portal to `document.body` for stacking context. - **Chakra UI** - `DefaultPortalManager` appends to a custom `#chakra-portal-root`. - **Headless UI** - Menu and Combobox portal for focus trap outside parent. - **React Bootstrap** - Popover and Tooltip use portals to escape table cells. ### Follow-up questions **Q:** Do portals preserve React context from parent providers? **A:** Yes. Context reads from the React fiber tree, not the DOM tree. A portal child sees all ancestor providers even though it renders into a different DOM node. **Q:** How does event bubbling work with portals? **A:** Native DOM events bubble through the DOM. React's synthetic events proxy through the React root. So `onClick` on a portal element bubbles through its React ancestors, not its DOM ancestors. **Q:** What's the difference between a portal and Shadow DOM? **A:** Portals keep global styles and React events connected. Shadow DOM creates a fully isolated style and event boundary. Use Shadow DOM for true third-party widget isolation; use portals to escape CSS constraints while staying in the React tree. **Q:** How do you handle portals in SSR (Next.js)? **A:** Use a `mounted` state flag set in `useEffect`. Return a hidden placeholder during server render. This prevents hydration mismatch errors in React 18 strict mode. ## Examples ### Confirmation modal with scroll lock ```jsx import { createPortal } from 'react-dom'; import { useEffect } from 'react'; function ConfirmationModal({ isOpen, onConfirm, onCancel, message }) { useEffect(() => { if (isOpen) document.body.style.overflow = 'hidden'; // lock scroll return () => { document.body.style.overflow = ''; }; }, [isOpen]); if (!isOpen) return null; return createPortal( <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 9999 }}> <div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)' }}> <p>{message}</p> <button onClick={onConfirm}>Confirm</button> <button onClick={onCancel}>Cancel</button> </div> </div>, document.body ); } ``` The modal renders directly on `document.body`, so no parent `overflow`, `transform`, or `z-index` can clip it. Scroll lock prevents background scrolling while the modal is open. This is the same pattern used in react-modal and Material-UI Dialog. ### SSR-safe portal ```jsx import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; function ClientOnlyPortal({ children }) { const [mounted, setMounted] = useState(false); const containerRef = useRef(null); useEffect(() => { containerRef.current = document.getElementById('portal-root'); setMounted(true); }, []); if (!mounted) return <div style={{ visibility: 'hidden' }}>{children}</div>; return createPortal(children, containerRef.current); } ``` Without the `mounted` guard, the server renders one thing and the client another. React 18 throws a hydration error. The hidden placeholder keeps DOM structure consistent across server and client, so React can reconcile without complaints.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.