React / React Portal
Home /React /React Portal

React Portal

What is a React Portal?

A React Portal is a built-in React feature that lets you render a child component
into a different DOM node than its parent component, while still keeping it inside the same
React component tree (and inheriting context, state, and event bubbling from its logical parent).

In simple words: React normally renders everything inside a single root DOM element (usually
<div id="root"></div>). A Portal breaks out of this root and injects
the component’s output into any other DOM node on the page.

Official React Definition (docs.react.dev):
“Portals provide a first-class way to render children into a DOM node that exists outside the
DOM hierarchy of the parent component.”

React Portals were introduced in React 16.0 (September 2017) and remain
unchanged and fully supported in React 18+.

2. Why Do We Need Portals?

React’s default rendering mounts all components inside one root div. This works perfectly for
most UIs. But some UI elements, like modals, tooltips, dropdowns, and drawers, need to live
outside the normal document flow for visual and behavioural reasons:

  • z-index stacking problems: If a modal is nested deep in the DOM, a parent
    element with overflow: hidden or a lower z-index can clip or hide it.
  • CSS overflow clipping: Tooltips and dropdowns can be cut off by
    overflow: hidden ancestors.
  • Visual independence: Notifications and overlays must appear above all other
    content regardless of where the triggering component sits in the tree.
  • Positioning relative to viewport: Fixed-position overlays must be free from
    CSS transform or filter ancestors that break position: fixed.

Portals solve all of these problems by injecting the rendered output into a DOM node at the top
level (usually directly inside <body>), while React still treats the component
as part of its logical tree.

3. The Problem Portals Solve

Imagine this component tree:


<App>
  <Sidebar>              <-- overflow: hidden; position: relative
    <UserCard>
      <Modal />          <-- supposed to fill the entire screen
    </UserCard>
  </Sidebar>
</App>

Without Portals, the Modal is trapped inside the Sidebar DOM node. Its CSS will be restricted
by the parent’s overflow: hidden and lower stacking context. The modal appears
clipped or behind other elements.

With a Portal, the Modal is rendered like this in the real DOM:


<body>
  <div id="root">
    <!-- App, Sidebar, UserCard are here -->
  </div>

  <div id="modal-root">
    <!-- Modal rendered here — free from parent constraints -->
  </div>
</body>

But React still treats the Modal as a child of UserCard for event bubbling, context, and state
management. Best of both worlds.

4. How React Portals Work Internally

When you use a Portal, React maintains two separate trees:

  1. React Component Tree (Logical/Virtual): The Portal’s component lives here as
    a normal child. Events, Context, and props flow through this tree as usual.
  2. Real DOM Tree (Physical): React mounts the Portal’s output into a different
    DOM node. This is only about where the HTML markup lives — not about React’s internal
    reconciliation.

React’s reconciler handles both: it knows that some components are portals and directs their
rendered output to the target DOM node, while still keeping their place in the fiber tree
(React’s internal representation of the component tree).

Key facts about Portal internals:

  • Portals participate in React’s lifecycle normally (componentDidMount, useEffect, etc.).
  • They trigger re-renders when parent state changes, just like regular children.
  • Unmounting the parent unmounts the portal’s output from the target DOM node.
  • React uses the target container as a host for the rendered subtree; it does not clone or copy,  it mounts directly.

Accessibility in Portals

Moving content to a different DOM node creates accessibility challenges because screen readers
follow the DOM structure, not React’s component tree. Here are the mandatory rules:

Required ARIA Attributes

Attribute Element Purpose
role="dialog" Modal container Tells screen readers this is a dialog window
aria-modal="true" Modal container Prevents screen readers from browsing content outside the modal
aria-labelledby Modal container Points to the modal’s heading element ID
aria-describedby Modal container Points to an element describing the modal’s purpose
role="tooltip" Tooltip container Identifies tooltip content to screen readers
aria-live="polite" Toast container Announces new toasts without interrupting current reading
aria-live="assertive" Error toast container Interrupts immediately for critical error announcements

Accessibility Checklist for Portals

  • Move focus into the portal on open; return it to the trigger on close.
  • Trap Tab/Shift+Tab within dialogs.
  • Support Escape key to close all overlay portals.
  • Use aria-modal="true" on dialogs to restrict screen reader browsing.
  • Use aria-live on notifications.
  • Ensure sufficient colour contrast inside the portal’s overlay content.
  • Do not hide important page content with aria-hidden when it is still accessible.

Common Mistakes

Mistake 1: Importing from wrong package


// WRONG
import { createPortal } from 'react';

// CORRECT
import { createPortal } from 'react-dom';

Mistake 2: Target element doesn’t exist


// WRONG – if #modal-root is not in HTML, this returns null and throws
createPortal(<Modal />, document.getElementById('modal-root'))

// CORRECT – check before using
const el = document.getElementById('modal-root');
if (el) return createPortal(<Modal />, el);
return null;

Mistake 3: Creating the DOM node on every render


// WRONG – creates a new div on every render, attaches to body repeatedly
function MyPortal({ children }) {
  const el = document.createElement('div');      // new node every render!
  document.body.appendChild(el);
  return createPortal(children, el);
}

// CORRECT – use useMemo or module-level variable
const portalRoot = document.createElement('div');
document.body.appendChild(portalRoot);

function MyPortal({ children }) {
  return createPortal(children, portalRoot);
}

Mistake 4: Forgetting to handle SSR


// WRONG in Next.js SSR – document is not defined on server
const container = document.getElementById('modal-root'); // ReferenceError

// CORRECT – guard with typeof check or useEffect
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;

Mistake 5: Skipping cleanup in useEffect


// WRONG – event listener leaks on unmount
useEffect(() => {
  document.addEventListener('keydown', handleKey);
  // No cleanup!
}, []);

// CORRECT
useEffect(() => {
  document.addEventListener('keydown', handleKey);
  return () => document.removeEventListener('keydown', handleKey);
}, []);

Mistake 6: Assuming CSS inherits through portal


// CSS from the React parent does NOT apply to portal content
// because the portal content is in a different DOM location.
// You must apply styles directly to the portal's content or its DOM container.

Best Practices

  1. Declare portal containers in index.html: Keep them as siblings of
    #root. Never nest them inside other elements.
  2. Create one container per purpose: Use #modal-root,
    #toast-root, #tooltip-root rather than dumping everything into
    document.body.
  3. Always handle SSR: Guard document access with
    useEffect or a mounted state flag.
  4. Include full accessibility: ARIA roles, keyboard navigation,
    focus management, these are not optional for overlays.
  5. Clean up event listeners: Return cleanup functions from every
    useEffect that attaches to document.
  6. Conditionally render: Return null when the portal is
    not needed rather than keeping it mounted empty.
  7. Stable container reference: Use useMemo or module-level
    variable — don’t create a new DOM node on every render.
  8. Combine Portals with Context: Expose addModal/addToast APIs through
    Context so any component can trigger overlays without prop drilling.
  9. Test with screen readers: VoiceOver, NVDA, or axe DevTools should
    confirm that portal content is announced correctly.
  10. Document z-index strategy: Define a project-wide z-index scale
    (e.g. modal: 9999, toast: 99999, tooltip: 999999) to prevent stacking conflicts.

FAQs

Q1. Do Portals work with React’s Context API?

Yes, fully. Even though the portal’s DOM output is outside the parent’s DOM
node, the portal component still lives in the React component tree. It can read any Context
value provided by an ancestor in that tree.

Q2. Do events inside a Portal bubble up to its React parent?

Yes. Events bubble through the React component tree, not the DOM tree.
A click inside a portal will trigger onClick on the portal’s React parent
component, even though the DOM parent is different. Use e.stopPropagation() if
you want to prevent this.

Q3. Can I use multiple Portals in one application?

Yes. You can have as many portals as you like, each targeting different DOM
containers. Common setups have separate containers for modals, toasts, and tooltips.

Q4. Can a Portal render into a DOM node that is itself inside React’s root?

Technically yes, but it creates confusing and circular behaviour. The portal
target should always be a separate DOM node outside #root to achieve the desired
isolation.

Q5. Is ReactDOM.createPortal available in React Native?

No. createPortal is specific to react-dom and is
only available in web environments. React Native has its own patterns for overlays
(e.g. Modal component from react-native).

Q6. What happens to the portal when the parent component unmounts?

The portal is also unmounted. React handles cleanup automatically. The portal’s
DOM nodes are removed from the target container, and all useEffect cleanup
functions are called.

Q7. Can I use portals in React Server Components (RSC)?

No. Server Components do not support portals because they don’t have access to
the browser DOM. Mark your portal components with 'use client' directive in
Next.js App Router.

Q8. Can a Portal contain another Portal?

Yes. You can nest portals, a portal component can itself render another portal.
Each portal renders into its specified container independently.

Q9. Does using document.body as the portal target cause any problems?

Using document.body works and is common for tooltips and dropdowns. The risk is
that many portals rendering directly into body can make DOM inspection messy.
For modals and toasts, prefer dedicated named containers for clarity and easier CSS targeting.

Q10. How do I animate portal content (fade in / slide in)?

Animate portal content the same way you would animate any React component: with CSS transitions,
keyframes, or libraries like Framer Motion. Since the portal renders real DOM nodes, all CSS
animation techniques work. The animation should be applied to the content wrapper inside the
portal:


/* styles.css */
.modal-enter {
  animation: fadeIn 200ms ease forwards;
}

@keyframes fadeIn {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}

{createPortal(
  <div className="modal-enter">
    {/* Modal content */}
  </div>,
  document.getElementById('modal-root')
)}

Q11. What is the key parameter in createPortal?

Added in React 18, the optional third argument key works like the key
prop on list items, it helps React identify and reconcile portals in a dynamic list.


{items.map(item =>
  createPortal(
    <Tooltip text={item.label} />,
    document.body,
    item.id   // <-- key
  )
)}

Q12. How are Portals different from rendering outside React entirely?

Rendering “outside React” (e.g. directly with document.createElement and
innerHTML) gives you a DOM node with no React integration — no state, no context,
no event system, no lifecycle. A Portal gives you a DOM node that lives outside the React root
but is still fully managed by React internally.

Q13. Can I use React.lazy with Portals?

Yes. You can lazy-load a component that uses or returns a portal just like
any other component:


const LazyModal = React.lazy(() => import('./Modal'));

<Suspense fallback={null}>
  <LazyModal isOpen={open} onClose={close} />
</Suspense>

Q14. Is there a performance difference between a Portal and a regular component?

The difference is negligible. The only overhead is one extra DOM element (the container node),
plus the browser reflow for fixed/absolute positioning. React’s reconciliation for the portal
subtree is identical to that for a regular component subtree.

Q15. How do I test components that use Portals in Jest / React Testing Library?

React Testing Library handles portals automatically, the rendered output, including portal
content, is queryable in the same document. You may need to add the portal container node
to your test setup:


// setupTests.js (runs before tests)
beforeEach(() => {
  const modalRoot = document.createElement('div');
  modalRoot.id = 'modal-root';
  document.body.appendChild(modalRoot);
});

afterEach(() => {
  document.getElementById('modal-root')?.remove();
});

Summary

Topic Key Point
Import import { createPortal } from 'react-dom'
Signature createPortal(children, container, key?)
Introduced React 16.0 (2017)
Physical DOM Renders into the specified container node
Logical tree Still part of the React component tree — context and events flow normally
Event bubbling Bubbles through React tree, NOT the DOM tree
CSS inheritance Follows the physical DOM, not the React tree
SSR Not supported on the server — guard with useEffect + mounted state
React Native Not available — web-only API
Server Components Not supported — requires 'use client'
Unmount behaviour Portal is unmounted when its parent component unmounts
Common uses Modals, Tooltips, Dropdowns, Toasts, Drawers
Accessibility Requires: role, aria-modal, focus management, keyboard handling
Performance Minimal overhead; use stable container references and conditional rendering
Testing RTL handles portals natively; add container node in test setup

References