Skip to content

Preact Portals — Rendering Outside the Parent DOM Tree

DodaTech Updated 2026-06-28 4 min read

Learn how to use portals in Preact to render components outside the parent DOM hierarchy for modals, tooltips, and overlays in the 3kB framework.

In this lesson, you'll understand what portals are, when to use them, and how to implement modals and tooltips that break out of overflow-hidden containers.

What You'll Learn

How to create portals with createPortal, render content to different DOM nodes, and use portals for modals, tooltips, and dropdowns that need to escape parent clipping.

Why It Matters

Sometimes a component needs to render outside its parent container: a modal that overlays everything, a tooltip that may overflow, or a dropdown that must appear above a scrollable list.

Real-World Use

DodaZIP's file extraction dialog uses a portal to render the progress modal above all other content, preventing CSS overflow or z-index issues from hiding the dialog.

flowchart TD
    A[App Root] --> B[Main Content]
    A --> C[Portal Container - #modal-root]
    B --> D[Button Click]
    D -->|Creates Portal| E[Modal in #modal-root]
    E -->|Independent Z-Index| F[Overlays Everything]
    style A fill:#673ab8,color:#fff
    style E fill:#4a148c,color:#fff

Creating a Portal

Preact's createPortal renders children into a different DOM node:

import { render, createPortal } from 'preact';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
      background: 'rgba(0,0,0,0.5)', display: 'flex',
      alignItems: 'center', justifyContent: 'center', zIndex: 9999
    }}>
      <div style={{ background: '#fff', padding: 24, borderRadius: 8, minWidth: 300 }}>
        {children}
        <button onClick={onClose} style={{ marginTop: 16 }}>Close</button>
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

Output: The modal renders into #modal-root, a sibling of the main app container. It overlays all content regardless of parent z-index or overflow settings.

Portal Container Setup

Add the portal target to your HTML:

<body>
  <div id="app"></div>
  <div id="modal-root"></div>
</body>

The #modal-root div sits outside the main app container, so portal content is free from parent CSS constraints.

Tooltip with Portal

import { useState, useRef } from 'preact/hooks';
import { createPortal } from 'preact';

function Tooltip({ text, children }) {
  const [visible, setVisible] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);

  const show = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPos({ top: rect.bottom + 8, left: rect.left + rect.width / 2 });
    }
    setVisible(true);
  };

  return (
    <span ref={triggerRef} onMouseEnter={show} onMouseLeave={() => setVisible(false)}>
      {children}
      {visible && createPortal(
        <div style={{
          position: 'fixed', top: pos.top, left: pos.left,
          transform: 'translateX(-50%)',
          background: '#333', color: '#fff',
          padding: '4px 8px', borderRadius: 4,
          fontSize: 12, whiteSpace: 'nowrap', zIndex: 9999
        }}>
          {text}
        </div>,
        document.getElementById('tooltip-root')
      )}
    </span>
  );
}

Output: The tooltip appears above any parent boundaries, not clipped by overflow: hidden on a parent container.

Portal Events and Context

Portals preserve the Preact component tree for context and events:

import { createContext, createPortal } from 'preact';

const ThemeContext = createContext('light');

function ThemedModal({ isOpen, onClose }) {
  const theme = useContext(ThemeContext);
  if (!isOpen) return null;

  return createPortal(
    <div style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff',
      padding: 24
    }}>
      <p>This modal respects the theme context.</p>
      <button onClick={onClose}>Close</button>
    </div>,
    document.getElementById('modal-root')
  );
}

Output: The modal receives context from the component tree where the portal is declared, not from the DOM position.

Common Mistakes

  1. Forgetting the portal container element: If document.getElementById('modal-root') returns null, the portal crashes. Always add the container to HTML.
  2. Using portals when CSS suffices: For simple z-index issues, try CSS solutions first. Portals add complexity and should only be used when CSS can't solve the problem.
  3. Not handling event propagation: Click events inside a portal still bubble through the Preact tree. Use stopPropagation() to prevent unwanted triggers.
  4. Rendering portals server-side: Portals access the DOM. During SSR, guard portal rendering with a browser check.
  5. Creating too many portal roots: One or two portal containers are enough. Separate containers for each feature add unnecessary DOM nodes.

Practice Questions

  1. What does createPortal do? Answer: Renders children into a different DOM node than the parent while preserving the Preact component tree hierarchy.

  2. Why do portals preserve context? Answer: Portals are rendered within the Preact component tree, not the DOM tree. Context flows through the component hierarchy.

  3. What is the most common use case for portals? Answer: Modals, tooltips, dropdowns, and any overlay that needs to escape parent CSS constraints like overflow: hidden or z-index stacking.

  4. What happens if the portal container doesn't exist in the DOM? Answer: createPortal throws an error because it can't find the target node. Always ensure the container exists.

Challenge

Build a context menu component that appears at the right-click position. Use a portal to render the menu above all content and close it when clicking outside.

Mini Project

Create a notification system where toasts appear in a portal container. Support multiple simultaneous toasts with auto-dismiss, positioned at the top-right of the viewport.

FAQ

Does Preact's createPortal differ from React's?

: The API is identical. Both use createPortal(children, domNode) and preserve component context.

Can I render multiple components into one portal container?

: Yes. Multiple portals can target the same container. Their content is appended as siblings.

Do portal events bubble to the parent Preact tree?

: Yes. Events bubble through the Preact component tree, not the DOM tree. A click inside a portal can trigger a parent's onClick handler.

Are portals compatible with server-side rendering?

: No. Portals require a DOM node reference. During SSR, skip portal rendering or use a fallback.

What's Next

Learn about Preact Compatibility Layer to use React libraries with Preact through preact/compat.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro