Teleport

Render components to different parts of the DOM tree (Portal).

The Teleport component (also known as a Portal) renders its children into a DOM node that exists outside the parent component's hierarchy. This is essential for modals, tooltips, and overlays that need to break out of overflow: hidden or z-index stacks.

Installation

npm install @zayne-labs/ui-react

Preview

Loading...

Basic Usage

The most common use case is rendering overlays at the end of the document body.

import { Teleport } from "@zayne-labs/ui-react/common/teleport";

function Modal({ children }) {
  return (
    <Teleport to="body">
      <div className="modal-overlay">{children}</div>
    </Teleport>
  );
}

Target a specific designated portal container in your HTML.

<Teleport to="#notifications-layer">
  <Toast message="Order Shipped!" />
</Teleport>

Useful when you need to render into an element managed within the same React tree.

const containerRef = useRef(null);

return (
  <>
    <div ref={containerRef} />
    <Teleport to={containerRef}>
      <span>Sent to specific container</span>
    </Teleport>
  </>
);

Control Insert Position

By default, Teleport renders its children as the last child of the target container. You can use the insertPosition prop to control exactly where the content is inserted relative to the target.

The insertPosition prop accepts the same values as the first argument of the native Element.insertAdjacentElement() DOM API.

beforeend: Inserts as the last child of the target. (Same as default but via a wrapper)

<Teleport to="#container" insertPosition="beforeend">
  <span>Appended inside</span>
</Teleport>

afterbegin: Inserts as the first child of the target.

<Teleport to="#container" insertPosition="afterbegin">
  <span>Prepended inside</span>
</Teleport>

beforebegin: Inserts immediately before the target element itself.

<Teleport to="#container" insertPosition="beforebegin">
  <span>Sibling before target</span>
</Teleport>

afterend: Inserts immediately after the target element itself.

<Teleport to="#container" insertPosition="afterend">
  <span>Sibling after target</span>
</Teleport>

Component Source

"use client";import { isString, type AnyString } from "@zayne-labs/toolkit-type-helpers";import { useLayoutEffect, useRef, useState } from "react";import { createPortal } from "react-dom";type ValidHtmlTags = keyof HTMLElementTagNameMap;export type TeleportProps = {	children: React.ReactNode;	insertPosition?: InsertPosition;	to: AnyString | HTMLElement | React.RefObject<HTMLElement> | ValidHtmlTags | null;};const TELEPORT_KEY = "teleport-wrapper";const getDestination = (to: NonNullable<TeleportProps["to"]>) => {	if (isString(to)) {		return document.querySelector<HTMLElement>(to);	}	if (to instanceof HTMLElement) {		return to;	}	return to.current;};function Teleport(props: TeleportProps) {	const { children, insertPosition, to } = props;	const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);	const destinationRef = useRef<HTMLElement | null>(null);	const tempWrapperRef = useRef<HTMLDivElement | null>(null);	useLayoutEffect(() => {		if (!to) return;		// eslint-disable-next-line react-hooks/todo -- Ignore for now		destinationRef.current ??= getDestination(to);		if (!destinationRef.current) return;		if (!insertPosition) {			setPortalContainer(destinationRef.current);			return;		}		// eslint-disable-next-line react-hooks/todo -- Ignore for now		tempWrapperRef.current ??= document.createElement("div");		tempWrapperRef.current.dataset.id = TELEPORT_KEY;		tempWrapperRef.current.style.display = "contents";		destinationRef.current.insertAdjacentElement(insertPosition, tempWrapperRef.current);		// const timeoutId = setTimeout(() => {		// 	tempWrapperRef.current.replaceWith(...tempWrapper.children);		// }, 0);		setPortalContainer(tempWrapperRef.current);		return () => {			tempWrapperRef.current?.remove();		};	}, [to, insertPosition]);	return portalContainer && createPortal(children, portalContainer, TELEPORT_KEY);}export { Teleport };

Component API

Why use Teleport?

  • Escape Clipping: Break out of parent containers with overflow: hidden or clip-path.
  • Z-Index Layering: Move overlays to a common sibling layer (like body) to manage complex z-index stacks predictably.
  • Isomorphic Support: Automatically handles hydration so that the portal works correctly in both SSR and CSR.
<div id="app" style={{ overflow: "hidden" }}>
	{/* Tooltip would be clipped here without Teleport */}
	<Teleport to="body">
		<Tooltip />
	</Teleport>
</div>

Ensure the target element exists in the DOM. If teleporting to a selector or ref, the target must be rendered before the Teleport component attempts to teleport content.

Edit on GitHub

Last updated on

On this page