Working with Refs & PortalsEntry 5 of 6
Portals (createPortal)
createPortal(jsx, domNode) from react-dom renders JSX into a different place in the DOM while keeping it in the same position in the React component tree.
Notes
A component nested deep in the layout may need its markup elsewhere, typically at the end of <body>, to escape overflow: hidden, z-index stacking contexts, or for accessibility. createPortal takes two arguments: the JSX to render, and the target DOM node (usually found with document.getElementById).
The portal content still behaves as a child of its React parent: props, state, and context flow into it normally, and events bubble up through the React tree, not the DOM tree.
What / Why / When
- What
Rendering output outside the parent's DOM container.
- Why
Overlays must visually sit above everything regardless of where they're declared.
- When
Modals, tooltips, dropdown menus, toasts.
- Use case
A result modal declared inside a game component but rendered into
#modal-root.
Code compare
<!-- index.html --><body> <div id="root"></div> <div id="modal-root"></div></body>import type { ReactNode } from "react";import { createPortal } from "react-dom";export function Modal({ children }: { children: ReactNode }) { const modalRoot = document.getElementById("modal-root"); if (!modalRoot) return null; return createPortal(<div className="modal">{children}</div>, modalRoot);}export function App() { return ( <div> <h1>App Content</h1> <Modal> <p>This is inside a portal!</p> </Modal> </div> );}Putting it together: a portal + useImperativeHandle + the native <dialog> element, a common pattern for modals:
import { useImperativeHandle, useRef, type ReactNode, type Ref } from 'react';import { createPortal } from 'react-dom';export type ModalHandle = { open: () => void; close: () => void };type ModalProps = { ref?: Ref<ModalHandle>; children: ReactNode };export function Modal({ ref, children }: ModalProps) { const dialogRef = useRef<HTMLDialogElement>(null); useImperativeHandle(ref, () => ({ open: () => dialogRef.current?.showModal(), close: () => dialogRef.current?.close(), })); return createPortal( <dialog ref={dialogRef} className="modal"> {children} <form method="dialog"> <button>Close</button> </form> </dialog>, document.getElementById('modal-root')!, );}// Parentconst modalRef = useRef<ModalHandle>(null);<Modal ref={modalRef}>Game over!</Modal><button onClick={() => modalRef.current?.open()}>Show result</button>The Angular CDK provides portals and overlays. For modals, the CDK Dialog service is the simplest option; it renders the component in an overlay container at the end of <body>:
npm install @angular/cdkimport { Component, inject } from "@angular/core";import { Dialog, DialogRef } from "@angular/cdk/dialog";@Component({ selector: "app-result-modal", template: ` <div class="modal"> <p>This is inside a portal!</p> <button (click)="dialogRef.close()">Close</button> </div> `,})export class ResultModalComponent { dialogRef = inject(DialogRef);}@Component({ selector: "app-root", template: ` <h1>App Content</h1> <button (click)="openModal()">Show result</button> `,})export class AppComponent { private dialog = inject(Dialog); openModal() { this.dialog.open(ResultModalComponent); }}For lower-level control (tooltips, custom positioning), use Overlay with a ComponentPortal or TemplatePortal from @angular/cdk/overlay and @angular/cdk/portal. ViewContainerRef.createComponent() creates components dynamically but inserts them at that container's location, so on its own it isn't a portal.
Differences and gotchas
- React portals are built into
react-dom. Angular's are in the CDK, a separate package. - React opens modals declaratively (render the portal) or imperatively (via a handle). The CDK
Dialogis service-driven: you callopen()with a component class. - The native
<dialog>element opened withshowModal()renders in the browser's top layer, above all other content. For simple modals, it removes the stacking problem in both frameworks, with or without a portal. - React portal events bubble through the React tree. A click inside a portal can trigger an
onClickon a React ancestor even though the DOM nodes aren't nested.
Quick reference
| React | Angular |
|---|---|
createPortal(jsx, node) | CDK Dialog.open() / Overlay + ComponentPortal |
<div id="modal-root"> in index.html | CDK overlay container (created automatically) |
dialogRef.current?.showModal() | dialog.open(Component) or native showModal() |