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.

react.devcreatePortal

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

React
<!-- index.html --><body>  <div id="root"></div>  <div id="modal-root"></div></body>
React
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:

React
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>:

Angular
npm install @angular/cdk
Angular
import { 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 Dialog is service-driven: you call open() with a component class.
  • The native <dialog> element opened with showModal() 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 onClick on a React ancestor even though the DOM nodes aren't nested.

Quick reference

ReactAngular
createPortal(jsx, node)CDK Dialog.open() / Overlay + ComponentPortal
<div id="modal-root"> in index.htmlCDK overlay container (created automatically)
dialogRef.current?.showModal()dialog.open(Component) or native showModal()