Preact Portals — Rendering Outside the Parent DOM Tree
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
- Forgetting the portal container element: If
document.getElementById('modal-root')returns null, the portal crashes. Always add the container to HTML. - 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.
- Not handling event propagation: Click events inside a portal still bubble through the Preact tree. Use
stopPropagation()to prevent unwanted triggers. - Rendering portals server-side: Portals access the DOM. During SSR, guard portal rendering with a browser check.
- Creating too many portal roots: One or two portal containers are enough. Separate containers for each feature add unnecessary DOM nodes.
Practice Questions
What does
createPortaldo? Answer: Renders children into a different DOM node than the parent while preserving the Preact component tree hierarchy.Why do portals preserve context? Answer: Portals are rendered within the Preact component tree, not the DOM tree. Context flows through the component hierarchy.
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: hiddenorz-indexstacking.What happens if the portal container doesn't exist in the DOM? Answer:
createPortalthrows 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
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