Portals + overlays cheatsheet.

createPortal

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,
  );
}

Renders children into a different DOM node, while keeping the React tree relationship (events bubble through React, not DOM).

Why portals

  • Modals that escape overflow: hidden parents.
  • Tooltips/popovers that must not be clipped.
  • Nested z-index hell.

Mount node

// Set up a stable target
function getModalRoot() {
  let el = document.getElementById("modal-root");
  if (!el) {
    el = document.createElement("div");
    el.id = "modal-root";
    document.body.appendChild(el);
  }
  return el;
}

createPortal(content, getModalRoot());

Or in index.html:

<body>
  <div id="root"></div>
  <div id="modal-root"></div>
</body>
function Modal({ open, onClose, children }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, onClose]);
  
  if (!open) return null;
  
  return createPortal(
    <div role="dialog" aria-modal="true" className="overlay" onClick={onClose}>
      <div className="dialog" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body,
  );
}

Focus trap

npm i focus-trap-react
import FocusTrap from "focus-trap-react";

<FocusTrap focusTrapOptions={{ initialFocus: false }}>
  <div role="dialog">
    <button>Close</button>
    <input />
  </div>
</FocusTrap>

Or use react-aria / shadcn / Radix dialogs which handle this for you.

Body scroll lock

useEffect(() => {
  if (!open) return;
  const original = document.body.style.overflow;
  document.body.style.overflow = "hidden";
  return () => { document.body.style.overflow = original; };
}, [open]);

<dialog> element (preferred today)

const ref = useRef<HTMLDialogElement>(null);

function open() { ref.current?.showModal(); }
function close() { ref.current?.close(); }

<dialog ref={ref}>
  <p>Hello</p>
  <button onClick={close}>OK</button>
</dialog>

Native focus trap, ESC handling, scroll lock. Style via ::backdrop.

Popover positioning

npm i @floating-ui/react
import { useFloating, autoUpdate, offset, flip, shift } from "@floating-ui/react";

function Popover({ children, content }) {
  const [open, setOpen] = useState(false);
  const { refs, floatingStyles } = useFloating({
    open,
    onOpenChange: setOpen,
    placement: "bottom",
    middleware: [offset(8), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
  
  return (
    <>
      <button ref={refs.setReference} onClick={() => setOpen(!open)}>
        {children}
      </button>
      {open && (
        <div ref={refs.setFloating} style={floatingStyles}>
          {content}
        </div>
      )}
    </>
  );
}

Use a UI library

Don’t roll your own modal/popover for production. Use:

  • Radix UI: unstyled, accessible primitives.
  • Headless UI: similar.
  • shadcn/ui: Radix + Tailwind, copy-into-repo.
  • react-aria: most rigorous accessibility.

Accessibility is hard. Outsource it.

Toast pattern

npm i sonner
import { Toaster, toast } from "sonner";

<Toaster />
toast.success("Saved!");

Tooltip

import { Tooltip } from "@radix-ui/react-tooltip";

<Tooltip>
  <Tooltip.Trigger>Hover me</Tooltip.Trigger>
  <Tooltip.Content>tip text</Tooltip.Content>
</Tooltip>

When NOT to portal

  • Inline content that doesn’t need to escape clipping.
  • Components meant to be styled by parent context (some inheritance breaks across portals).

Common mistakes

  • Portal without ARIA — screen readers miss it.
  • Missing focus trap — Tab escapes modal.
  • Body scroll not locked — background scrolls.
  • Click on overlay not stopped at dialog → closes when clicking inside.
  • Z-index war instead of using portal.

Read this next

If you want my modal + popover patterns, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .