Portals, Modals and Overlays
AdvancedA portal renders a child into a different DOM node while keeping it in the React tree. It is how modals escape overflow and stacking contexts — and it is the easy half of building one.
Overview
Every overlay hits the same wall: a modal rendered inside a card is clipped by that card's overflow, or trapped beneath a sibling because of a stacking context it cannot escape. A portal solves that by putting the DOM node elsewhere — usually document.body — while events still propagate through the React tree, which is exactly the behaviour you want. What a portal does not give you is any of the accessibility: focus trapping, focus restoration, Escape, scroll locking and correct announcement are all yours to build, which is the real argument for using a library.
What a Portal Does
A different DOM position, the same React tree.
import { createPortal } from 'react-dom'
function Modal({ children, onClose }) {
return createPortal(
<div className="backdrop" onClick={onClose}>
<div role="dialog" aria-modal="true" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body, // where it renders
)
}
// DOM position: document.body — free of any ancestor's overflow,
// transform, filter or z-index stacking context.
// React position: still a child of the component that rendered it,
// so context works and events bubble to the React parent.
// That last part surprises people: a click inside the portal fires
// the onClick of the React ancestor, even though the DOM nodes are
// nowhere near each other.
// Portals are also right for tooltips, dropdowns, popovers and
// toasts — anything that must not be clipped by its container.What a Modal Actually Requires
The checklist. Missing any of it makes the dialog unusable for someone.
// 1. Focus moves into the dialog on open
// 2. Focus is TRAPPED inside while it is open (Tab cannot escape)
// 3. Focus RETURNS to the trigger on close
// 4. Escape closes it
// 5. The background does not scroll
// 6. role="dialog" aria-modal="true" and an accessible name
// 7. Content behind is inert for screen readers
useEffect(() => {
const previouslyFocused = document.activeElement
dialogRef.current?.focus()
const onKey = (e) => e.key === 'Escape' && onClose()
document.addEventListener('keydown', onKey)
const { overflow } = document.body.style
document.body.style.overflow = 'hidden' // lock scroll
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = overflow
previouslyFocused?.focus() // restore focus
}
}, [onClose])
<div role="dialog" aria-modal="true" aria-labelledby="title" tabIndex={-1}>
// Focus trapping is the fiddly one, which is why the honest advice
// is to use Radix, React Aria or Headless UI rather than to rebuild
// it per project.Layering and Native Alternatives
Stacking, scroll locking on iOS, and the platform dialog.
// A single portal root keeps ordering predictable
<div id="portal-root" /> // in index.html, after #root
// Layer with tokens rather than escalating numbers
--z-dropdown: 100; --z-modal: 200; --z-toast: 300;
// The z-index arms race (9999, 99999) is a symptom of stacking
// contexts, not of numbers being too small.
// A modal opening a confirm dialog needs an ordered stack, not two
// components each assuming they are on top.
// The native <dialog> now does much of this in every modern browser
const ref = useRef(null)
ref.current?.showModal() // focus trap, Escape and ::backdrop free
<dialog ref={ref} onClose={onClose}>...</dialog>
// Still verify focus restoration and scroll locking.
// iOS scroll locking with overflow:hidden alone lets the background
// scroll behind the modal — position: fixed on the body, restoring
// the scroll offset on close, is the usual workaround.Key Points to Remember
- 1A portal changes where a child renders in the DOM while keeping it in the React tree, so context and event bubbling still work
- 2Portals free overlays from ancestor overflow, transform and z-index stacking contexts
- 3A usable modal needs focus move, focus trap, focus restore, Escape, scroll lock and correct ARIA — the portal is the easy part
- 4Use a headless library for dialogs rather than rebuilding focus trapping in every project
- 5Layer overlays with z-index tokens from a single portal root; escalating numbers indicate a stacking-context problem
Interview Questions
Sign in to ask AriaWhat problem does createPortal solve, and where do events go?
What does a modal need beyond rendering into document.body?
Why does a modal sometimes render behind other content despite a high z-index?
Ask Aria about Portals, Modals and Overlays
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.