React Portals
Render child components into external DOM nodes outside parent hierarchy using createPortal without losing React context.
TL;DR
- Teleport elements into distant DOM containers using
createPortal. - Escape restrictive CSS overflow and
z-indexstacking boundaries easily. - Preserve React context and synthetic
eventbubbling across portal gaps.
Portal Fundamentals
createPortal APIMount JSX into specific external DOM target element.
import { createPortal } from 'react-dom';
export function Overlay({ children }: Props) {
return createPortal(children, document.body);
}Mounted DOM CheckEnsure container node exists before rendering portal.
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return createPortal(children, document.body);Dynamic Target ContainerCreate and clean up dedicated mount element dynamically.
useEffect(() => {
const el = document.createElement('div');
document.body.appendChild(el);
return () => { document.body.removeChild(el); };
}, []);Event Bubbling and Context
Synthetic Event BubblingParent catches clicks originating inside portal child.
export function Shell() {
const onPortalClick = () => {
console.log('Clicked in portal');
};
return (
<CardView onClick={onPortalClick}>
<PortalModal />
</CardView>
);
}Context TransparencyContext passes seamlessly through portal boundaries.
export function PortalContent() {
const theme = useContext(ThemeContext);
return (
<Text color={theme.accent}>Portaled</Text>
);
}Escape Overflow ConstraintsBreak free from ancestor hidden overflow clipping.
// Renders outside overflow: hidden container
return createPortal(
<TooltipView text="Info" />,
document.body
);Floating Overlays and Tooltips
Positioned Floating PopoverRender absolute floating panel using measured coords.
return createPortal(
<PopoverView style={{ top: y, left: x }}>
<Text>Popover</Text>
</PopoverView>,
document.body
);Backdrop Overlay MaskRender dark backdrop behind modal outside hierarchy.
return createPortal(
<Backdrop onClick={onClose} />,
document.body
);Multiple Dedicated Portal RootsSeparate portal targets for modals and tooltips.
<PortalRoot id="modal-root" />
<PortalRoot id="tooltip-root" />Portal Best Practices
Accessibility Dialog RolesApply proper ARIA role and modal state attributes.
<DialogView
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
/>Escape Key HandlingListen for escape keydown to close overlay.
useEffect(() => {
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onEsc);
return () => {
window.removeEventListener('keydown', onEsc);
};
}, [onClose]);Focus RestorationReturn focus to trigger button upon portal close.
const prevFocus = useRef(document.activeElement);
useEffect(() => () => {
(prevFocus.current as HTMLElement)?.focus();
}, []);Tips
- Mount modals and floating tooltips directly into
document.bodyto avoid clipping issues caused by ancestoroverflow: hiddenrules. - Take advantage of React synthetic event bubbling through portals to capture child interactions in ancestor
onClickhandlers as normal.
Warnings
- Always verify that the target DOM element exists before invoking
createPortalto prevent null container mounting crashes. - Remember to manage keyboard focus trapping and aria-modal attributes when rendering dialogs into external portal
nodes.
In Practice
Create a complete modal overlay rendered directly into document.body with backdrop dismissal and escape key listening.
- Check that browser DOM is mounted before creating portal.
- Attach escape key listener to close dialog imperatively.
- Render backdrop and modal panel into document.body target.
- Clean up event listeners on modal dismissal.
export function Modal(props: any) {
const { isOpen, onClose, children } = props;
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('keydown', onKey);
};
}, [onClose]);
if (!isOpen) return null;
return createPortal(
<CardView role="dialog" aria-modal="true">
<Button onClick={onClose}>Close</Button>
{children}
</CardView>,
document.body
);
}FAQ
Yes. Even though a portal renders into an entirely different DOM node, React synthetic events bubble up through the virtual React component tree, not the physical browser DOM hierarchy.
React context remains fully intact across portals. Consumers rendered inside a portal read context values from their React parent tree exactly as if they were rendered in place.
Use createPortal whenever a child component needs to break visually out of its parent's CSS bounding box, such as full-screen modal overlays, floating dropdown menus, and hover tooltips.