stackademic

The leading education platform for anyone with an interest in software development.

React Portals

Render children into a DOM node outside the parent hierarchy

Overview

A portal renders children into a DOM node that lives outside the parent component's DOM tree, while keeping them in the React tree. This is ideal for modals, tooltips, and toasts that must escape a parent's overflow: hidden or z-index stacking context. Events still bubble through the React tree, not the DOM position.

Syntax / Usage

Call createPortal(children, domNode) and return its result from your component.

import { createPortal } from 'react-dom'

function Modal({ children, onClose }: { children: React.ReactNode; onClose: () => void }) {
  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div className="dialog" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  )
}

Examples

Render into a dedicated container element created once via a ref:

import { useRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'

function Toast({ message }: { message: string }) {
  const [mounted, setMounted] = useState(false)
  const nodeRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const node = document.createElement('div')
    document.body.appendChild(node)
    nodeRef.current = node
    setMounted(true)
    return () => {
      document.body.removeChild(node)
    }
  }, [])

  if (!mounted || !nodeRef.current) return null
  return createPortal(<div className="toast">{message}</div>, nodeRef.current)
}

Because events propagate through the React tree, a click inside a portalled modal still bubbles to the parent's onClick handler even though the DOM node lives on document.body.

Common Mistakes

  • Rendering a portal during SSR where document is undefined—guard with a mounted check
  • Forgetting to remove a dynamically created container, leaking DOM nodes
  • Assuming events bubble by DOM position instead of React tree position
  • Losing focus management and accessibility—portals still need focus traps and aria roles
  • Recreating the target container on every render instead of once in an effect

See Also

components use-ref use-effect