❓What is a React Portal?
React Portals let you render a component outside its parent’s DOM structure, even though it still behaves like a normal child in React.
Normally, a component is rendered inside its parent’s DOM element.
With a Portal, React can place that component somewhere else in the DOM—for example, directly inside <body> or a separate <div> in index.html.
The Syntax
The createPortal function is part of the react-dom package:
ReactDOM.createPortal(child, container)$$child: Anything React can render (JSX, a component, a string, or a fragment).container: A valid DOM element (where the child should be placed).
🧐 Why Use Portals? (The Problem)
In standard rendering, child components are constrained by their parents' CSS. Common issues include:
z-indexconflicts: A modal inside a deeply nested container might appear behind other elements because of its parent's stacking context.overflow: hidden: If a parent has this property, any child (like a tooltip or dropdown) that extends beyond the parent's boundaries will be cut off.
✅ Implementation Step-by-Step
Step A: Add a Target in index.html
Open your public/index.html and add a new div alongside your main root.
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>Step B: Create the Portal Component
import ReactDOM from 'react-dom';
const Modal = ({ children, onClose }) => {
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.getElementById('modal-root') // The target node
);
};Step C: Using the Portal in App.js
In your main component, you treat the Portal just like any other child component. You can pass props, manage its state, and even wrap it in logic.
import React, { useState } from 'react';
import Modal from './Modal'; // Import the portal component we created
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="app-container" style={{ overflow: 'hidden', position: 'relative' }}>
<h1>Main Application Content</h1>
<p>This container has "overflow: hidden", but our modal will still work!</p>
<button onClick={() => setIsOpen(true)}>Open Portal Modal</button>
{/* The Modal logic remains inside the parent's React tree */}
{isOpen && (
<Modal onClose={() => setIsOpen(false)}>
<h2>Portal Content</h2>
<p>I am rendered outside the "app-container" DOM node!</p>
</Modal>
)}
</div>
);
}
export default App;<div class="infographic-vscodeterm" id="vt">
<style>
.infographic-vscodeterm {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-deep: #1942a8;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--radius: 8px;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 900px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-vscodeterm[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #13171f;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-deep: #8fb4ff;
--accent-soft: #20293f;
--line: #2c313d;
}
.infographic-vscodeterm * { box-sizing: border-box; min-width: 0; }
.vt-eyebrow {
font-family: var(--mono-font); font-size: 0.72rem; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--accent); margin: 0 0 0.45rem;
display: flex; align-items: center; justify-content: center; gap: 0.4rem;
}
.vt-eyebrow::before { content: ""; width: 14px; height: 2px; background: var(--accent); display: inline-block; }
.vt-title {
font-family: var(--display-font); font-size: clamp(1.5rem, 4vw, 2.3rem); text-align: center;
margin: 0 0 0.5rem; line-height: 1.14; letter-spacing: -0.01em;
}
.vt-sub { color: var(--ink-soft); font-size: 0.95rem; text-align: center; margin: 0 0 1.7rem; }
/* ===== VS Code window — fixed dark theme regardless of page mode ===== */
.vt-window { border-radius: 8px; overflow: hidden; box-shadow: 0 16px 40px -14px rgba(0,0,0,0.45); border: 1px solid #1e1e1e; font-family: var(--mono-font); }
.vt-titlebar { background: #323233; display: flex; align-items: center; padding: 0.5rem 0.8rem; position: relative; }
.vt-dots { display: flex; gap: 0.4rem; }
.vt-dots span { width: 10px; height: 10px; border-radius: 50%; display: block; }
.vt-dots span:nth-child(1) { background: #ff5f56; }
.vt-dots span:nth-child(2) { background: #ffbd2e; }
.vt-dots span:nth-child(3) { background: #27c93f; }
.vt-titlebar-text { position: absolute; left: 50%; transform: translateX(-50%); font-size: 0.72rem; color: #cccccc; }
.vt-body { display: flex; height: 320px; }
.vt-resize-h { width: 5px; cursor: col-resize; background: transparent; flex-shrink: 0; transition: background 0.15s; touch-action: none; }
.vt-resize-h:hover, .vt-resize-h.is-dragging { background: #0078d4; }
.vt-resize-v { height: 5px; cursor: row-resize; background: transparent; flex-shrink: 0; transition: background 0.15s; touch-action: none; }
.vt-resize-v:hover, .vt-resize-v.is-dragging { background: #0078d4; }
.vt-window-resize {
height: 9px; background: #252526; cursor: row-resize; display: flex; align-items: center;
justify-content: center; border-top: 1px solid #1e1e1e; touch-action: none;
}
.vt-window-resize span { width: 30px; height: 3px; border-radius: 2px; background: #4a4a52; }
.vt-window-resize:hover span, .vt-window-resize.is-dragging span { background: #0078d4; }
.vt-toggle-btn {
background: none; border: none; color: #9a9a9a; cursor: pointer; display: flex; align-items: center;
padding: 0.3rem; border-radius: 3px; flex-shrink: 0;
}
.vt-toggle-btn:hover { background: #2a2d2e; color: #fff; }
.vt-toggle-btn svg { width: 13px; height: 13px; }
.vt-sidebar.is-hidden { width: 0 !important; overflow: hidden; border-right: none; }
.vt-terminal.is-collapsed { height: auto !important; }
.vt-terminal.is-collapsed .vt-term-body { display: none; }
.vt-terminal.is-collapsed .vt-term-toggle svg { transform: rotate(-90deg); }
.vt-activitybar { width: 40px; background: #333333; display: flex; flex-direction: column; align-items: center; padding-top: 0.7rem; gap: 1.1rem; flex-shrink: 0; }
.vt-activitybar svg { width: 18px; height: 18px; opacity: 0.55; }
.vt-activitybar svg.is-on { opacity: 1; }
.vt-sidebar { width: 160px; background: #252526; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #1e1e1e; }
.vt-sidebar-head { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; color: #bbbbbb; padding: 0.6rem 0.8rem 0.3rem; }
.vt-sidebar-root { font-size: 0.74rem; color: #cccccc; padding: 0.2rem 0.7rem; display: flex; align-items: center; gap: 0.3rem; }
.vt-sidebar-folder { font-size: 0.74rem; color: #cccccc; padding: 0.2rem 0.7rem 0.2rem 1.4rem; display: flex; align-items: center; gap: 0.3rem; }
.vt-row {
display: flex; align-items: center; gap: 0.35rem; width: 100%; background: none; border: none;
color: #cccccc; font: inherit; font-size: 0.74rem; padding: 0.24rem 0.7rem 0.24rem 2.1rem;
cursor: pointer; text-align: left; border-left: 2px solid transparent;
}
.vt-row:hover { background: #2a2d2e; }
.vt-row.is-selected { background: #37373d; border-left-color: #0078d4; }
.vt-badge { width: 15px; height: 15px; border-radius: 3px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.46rem; font-weight: 700; color: #1a1a1a; background: #29b6f6; }
.vt-badge.badge-html { background: #e37933; }
.vt-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.vt-tabbar { display: flex; background: #252526; border-bottom: 1px solid #1e1e1e; overflow-x: auto; }
.vt-tab {
display: flex; align-items: center; gap: 0.35rem; background: #1e1e1e; color: #808080; font: inherit;
font-size: 0.72rem; padding: 0.45rem 0.8rem; border: none; border-right: 1px solid #1e1e1e;
border-top: 2px solid transparent; cursor: pointer; white-space: nowrap;
}
.vt-tab.is-active { color: #d4d4d4; border-top-color: #0078d4; background: #1e1e1e; }
.vt-tab .vt-badge { width: 13px; height: 13px; font-size: 0.42rem; }
.vt-code-wrap { flex: 1; background: #1e1e1e; overflow: auto; padding: 0.85rem 1rem; }
.vt-code-wrap pre { margin: 0; }
.vt-code-wrap code { font-family: var(--mono-font); font-size: 0.71rem; line-height: 1.68; color: #d4d4d8; white-space: pre; }
.tok-kw { color: #c678dd; }
.tok-tag { color: #e06c75; }
.tok-type { color: #61afef; }
.tok-attr { color: #d19a66; }
.tok-str { color: #98c379; }
.tok-punc { color: #6b7280; }
.tok-comment { color: #7f848e; font-style: italic; }
/* ===== terminal ===== */
.vt-terminal { background: #181818; border-top: 1px solid #2c2c2c; display: flex; flex-direction: column; height: 150px; flex-shrink: 0; }
.vt-term-head { display: flex; align-items: center; gap: 0.4rem; font-size: 0.62rem; letter-spacing: 0.05em; color: #9a9a9a; padding: 0.35rem 0.8rem; border-bottom: 1px solid #262626; text-transform: uppercase; }
.vt-term-body { flex: 1; overflow-y: auto; padding: 0.5rem 0.9rem; font-size: 0.72rem; line-height: 1.65; }
.vt-term-line { color: #d4d4d4; white-space: pre; opacity: 0; transform: translateY(2px); animation: vt-line-in 0.25s ease forwards; }
@keyframes vt-line-in { to { opacity: 1; transform: none; } }
.vt-term-line.is-cmd { color: #7ee787; }
.vt-term-line.is-ok { color: #7ee787; font-weight: 600; }
.vt-term-prompt { color: #7ee787; }
.vt-run-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.2rem; }
.vt-run-btn {
font: inherit; font-size: 0.68rem; font-weight: 600; color: #1a1a1a; background: #7ee787;
border: none; border-radius: 4px; padding: 0.3rem 0.7rem; cursor: pointer; display: flex; align-items: center; gap: 0.3rem;
}
.vt-run-btn:disabled { opacity: 0.5; cursor: default; }
.vt-run-btn svg { width: 9px; height: 9px; }
.vt-run-hint { color: #6b7280; font-size: 0.68rem; }
@media (max-width: 620px) {
.vt-sidebar { width: 130px; }
.vt-body { height: 340px; }
}
@media (max-width: 480px) { .vt-activitybar { display: none; } }
/* ===== browser output ===== */
.vt-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center; margin: 1.5rem 0 0.7rem;
}
.vt-browser {
border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; max-width: 420px; margin: 0 auto;
box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18);
opacity: 0; transform: translateY(8px); transition: opacity 0.4s ease, transform 0.4s ease; pointer-events: none;
}
.vt-browser.is-shown { opacity: 1; transform: none; pointer-events: auto; }
.vt-browser-bar { background: #e9e9e9; padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; }
.vt-browser-bar .vt-dots span { width: 8px; height: 8px; }
.vt-browser-url { font-family: var(--mono-font); font-size: 0.65rem; color: #555; background: #fff; border-radius: 4px; padding: 0.2rem 0.5rem; flex: 1; }
.vt-output-placeholder { text-align: center; font-size: 0.82rem; color: var(--ink-soft); }
/* --- App output --- */
.vt-portal-page { background: #fff; padding: 1.5rem; position: relative; }
.vt-app-container {
overflow: hidden; position: relative; border: 2px dashed #cbd5e1; border-radius: 8px;
padding: 1.2rem; text-align: left;
}
.vt-app-container h1 { font-size: 1rem; margin: 0 0 0.5rem; color: #000; }
.vt-app-container p { font-size: 0.82rem; color: #555; margin: 0 0 1rem; line-height: 1.4; }
.vt-clip-tag {
position: absolute; top: 0.5rem; right: 0.5rem; font-family: var(--mono-font); font-size: 0.62rem;
color: #94a3b8; border: 1px solid #cbd5e1; border-radius: 100px; padding: 0.1rem 0.5rem;
}
.vt-open-btn {
font-family: var(--body-font); font-size: 0.85rem; font-weight: 600; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.55rem 1.1rem; cursor: pointer; transition: transform 0.15s;
}
.vt-open-btn:hover { transform: translateY(-1px); }
#vtModalRoot { position: relative; }
.vt-modal-overlay {
position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: none;
align-items: center; justify-content: center; z-index: 5; border-radius: 8px;
}
.vt-modal-overlay.is-open { display: flex; }
.vt-modal-content {
background: #fff; border-radius: 8px; padding: 1.3rem 1.4rem; max-width: 260px; text-align: center;
box-shadow: 0 12px 30px rgba(0,0,0,0.3);
}
.vt-modal-content h2 { font-size: 0.95rem; margin: 0 0 0.5rem; color: #000; }
.vt-modal-content p { font-size: 0.82rem; color: #444; margin: 0 0 1rem; line-height: 1.4; }
.vt-modal-close {
font-family: var(--body-font); font-size: 0.8rem; font-weight: 600; color: #fff; background: #dc2626;
border: none; border-radius: 100px; padding: 0.4rem 0.9rem; cursor: pointer;
}
.vt-modal-root-tag {
font-family: var(--mono-font); font-size: 0.6rem; color: #94a3b8; text-align: center; margin: 0.6rem 0 0;
}
.vt-takeaway {
margin-top: 1.7rem; padding: 0.95rem 1.2rem; background: var(--accent-soft); border-left: 3px solid var(--accent);
border-radius: 4px; font-size: 0.9rem; color: var(--ink); line-height: 1.5; max-width: 660px; margin-left: auto; margin-right: auto;
}
.vt-takeaway b { color: var(--accent-deep); }
.vt-takeaway code { font-family: var(--mono-font); }
@media (prefers-reduced-motion: reduce) {
.infographic-vscodeterm *, .infographic-vscodeterm *::before, .infographic-vscodeterm *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="vt-eyebrow">Portals</div>
<h1 class="vt-title">Escaping overflow: hidden with a Portal</h1>
<p class="vt-sub">Run the dev server, then open the modal — it renders outside the clipped container.</p>
<div class="vt-window">
<div class="vt-titlebar">
<div class="vt-dots"><span></span><span></span><span></span></div>
<div class="vt-titlebar-text">portal-demo — Visual Studio Code</div>
</div>
<div class="vt-body">
<div class="vt-activitybar">
<svg class="is-on" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="14" y="4" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="3" y="15" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="14" y="15" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/></svg>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="6.5" stroke="#fff" stroke-width="1.6"/><line x1="15" y1="15" x2="20" y2="20" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/></svg>
</div>
<div class="vt-sidebar">
<div class="vt-sidebar-head">EXPLORER</div>
<div class="vt-sidebar-root">▾ PORTAL-DEMO</div>
<div class="vt-sidebar-folder">▾ public</div>
<button class="vt-row" data-file="indexhtml" type="button"><span class="vt-badge badge-html"><></span><span>index.html</span></button>
<div class="vt-sidebar-folder">▾ src</div>
<button class="vt-row" data-file="modal" type="button"><span class="vt-badge">JS</span><span>Modal.js</span></button>
<button class="vt-row is-selected" data-file="app" type="button"><span class="vt-badge">JS</span><span>App.js</span></button>
</div>
<div class="vt-resize-h" id="vtSidebarHandle" title="Drag to resize sidebar"></div>
<div class="vt-main">
<div class="vt-tabbar">
<button class="vt-toggle-btn" id="vtToggleSidebar" type="button" aria-label="Toggle sidebar" title="Toggle sidebar">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="1.5" y="2" width="13" height="12" rx="1.5" stroke="currentColor" stroke-width="1.3"/><line x1="6" y1="2" x2="6" y2="14" stroke="currentColor" stroke-width="1.3"/></svg>
</button>
<button class="vt-tab" data-file="indexhtml" type="button"><span class="vt-badge badge-html"><></span><span>index.html</span></button>
<button class="vt-tab" data-file="modal" type="button"><span class="vt-badge">JS</span><span>Modal.js</span></button>
<button class="vt-tab is-active" data-file="app" type="button"><span class="vt-badge">JS</span><span>App.js</span></button>
</div>
<div class="vt-code-wrap">
<pre><code id="vtCode"></code></pre>
</div>
<div class="vt-resize-v" id="vtTerminalHandle" title="Drag to resize terminal"></div>
<div class="vt-terminal">
<div class="vt-term-head">
<button class="vt-toggle-btn vt-term-toggle" id="vtToggleTerminal" type="button" aria-label="Toggle terminal" title="Toggle terminal">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span>Terminal</span>
</div>
<div class="vt-term-body">
<div id="vtTermLines"><div class="vt-term-line" style="animation-delay:0s;"><span class="vt-term-prompt">$</span> </div></div>
<div class="vt-run-row">
<button class="vt-run-btn" id="vtRunBtn" type="button">
<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 1.5v9l7-4.5z" fill="currentColor"/></svg>
<span id="vtRunLabel">npm start</span>
</button>
<span class="vt-run-hint" id="vtRunHint">runs the dev server</span>
</div>
</div>
</div>
</div>
</div>
<div class="vt-window-resize" id="vtWindowHandle" title="Drag to resize window"><span></span></div>
</div>
<div class="vt-output-label">Output</div>
<div id="vtOutputArea">
<p class="vt-output-placeholder">Run <code style="font-family:var(--mono-font)">npm start</code> above to see it here.</p>
<div class="vt-browser" id="vtBrowser">
<div class="vt-browser-bar">
<div class="vt-dots"><span></span><span></span><span></span></div>
<div class="vt-browser-url">localhost:3000</div>
</div>
<div class="vt-portal-page">
<div class="vt-app-container" id="vtAppContainer">
<span class="vt-clip-tag">overflow: hidden</span>
<h1>Main Application Content</h1>
<p>This container has "overflow: hidden", but our modal will still work!</p>
<button class="vt-open-btn" id="vtOpenBtn" type="button">Open Portal Modal</button>
</div>
<!-- this sits OUTSIDE #vtAppContainer, mirroring #modal-root outside #root -->
<div id="vtModalRoot">
<div class="vt-modal-overlay" id="vtModalOverlay">
<div class="vt-modal-content">
<h2>Portal Content</h2>
<p>I am rendered outside the "app-container" DOM node!</p>
<button class="vt-modal-close" id="vtCloseBtn" type="button">Close</button>
</div>
</div>
</div>
<p class="vt-modal-root-tag">↑ this overlay lives in #modal-root, a sibling of #root — not inside the dashed box</p>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('vt');
if (!root) return;
/* --- dark mode detection: body attribute > cookie > postMessage > light --- */
function applyDark(dark) {
if (dark) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
applyDark(dark);
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') applyDark(!!e.data.value);
});
/* --- file switching --- */
var files = {
indexhtml:
'<span class="tok-punc"><</span><span class="tok-tag">body</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">id</span><span class="tok-punc">=</span><span class="tok-str">"root"</span><span class="tok-punc">></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">id</span><span class="tok-punc">=</span><span class="tok-str">"modal-root"</span><span class="tok-punc">></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
'<span class="tok-punc"></</span><span class="tok-tag">body</span><span class="tok-punc">></span>',
modal:
'<span class="tok-kw">import</span> <span class="tok-type">ReactDOM</span> <span class="tok-kw">from</span> <span class="tok-str">\'react-dom\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">const</span> <span class="tok-type">Modal</span> <span class="tok-punc">=</span> <span class="tok-punc">({</span> children<span class="tok-punc">,</span> onClose <span class="tok-punc">}) => {</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-type">ReactDOM</span>.createPortal<span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">className</span><span class="tok-punc">=</span><span class="tok-str">"modal-overlay"</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">className</span><span class="tok-punc">=</span><span class="tok-str">"modal-content"</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">{</span>children<span class="tok-punc">}</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>onClose<span class="tok-punc">}></span>Close<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">>,</span>\n' +
' document.getElementById<span class="tok-punc">(</span><span class="tok-str">\'modal-root\'</span><span class="tok-punc">)</span> <span class="tok-comment">// The target node</span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">};</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-type">Modal</span><span class="tok-punc">;</span>',
app:
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useState <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n' +
'<span class="tok-kw">import</span> <span class="tok-type">Modal</span> <span class="tok-kw">from</span> <span class="tok-str">\'./Modal\'</span><span class="tok-punc">;</span> <span class="tok-comment">// Import the portal component</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">App</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>isOpen<span class="tok-punc">,</span> setIsOpen<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-kw">false</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">className</span><span class="tok-punc">=</span><span class="tok-str">"app-container"</span> <span class="tok-attr">style</span><span class="tok-punc">={{</span> <span class="tok-attr">overflow</span><span class="tok-punc">:</span> <span class="tok-str">\'hidden\'</span><span class="tok-punc">,</span> <span class="tok-attr">position</span><span class="tok-punc">:</span> <span class="tok-str">\'relative\'</span> <span class="tok-punc">}}></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h1</span><span class="tok-punc">></span>Main Application Content<span class="tok-punc"></</span><span class="tok-tag">h1</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>This container has "overflow: hidden"<span class="tok-punc">,</span> but our modal will still work!<span class="tok-punc"></</span><span class="tok-tag">p</span><span class="tok-punc">></span>\n\n' +
' <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span><span class="tok-punc">()</span> <span class="tok-punc">=></span> setIsOpen<span class="tok-punc">(</span><span class="tok-kw">true</span><span class="tok-punc">)}></span>Open Portal Modal<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n\n' +
' <span class="tok-comment">{/* The Modal logic remains inside the parent\'s React tree */}</span>\n' +
' <span class="tok-punc">{</span>isOpen <span class="tok-punc">&&</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-type">Modal</span> <span class="tok-attr">onClose</span><span class="tok-punc">={</span><span class="tok-punc">()</span> <span class="tok-punc">=></span> setIsOpen<span class="tok-punc">(</span><span class="tok-kw">false</span><span class="tok-punc">)}></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h2</span><span class="tok-punc">></span>Portal Content<span class="tok-punc"></</span><span class="tok-tag">h2</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>I am rendered outside the "app-container" DOM node!<span class="tok-punc"></</span><span class="tok-tag">p</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-type">Modal</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">)}</span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-type">App</span><span class="tok-punc">;</span>'
};
var codeEl = root.querySelector('#vtCode');
var rows = root.querySelectorAll('.vt-row');
var tabs = root.querySelectorAll('.vt-tab');
function selectFile(id) {
codeEl.innerHTML = files[id];
rows.forEach(function (r) { r.classList.toggle('is-selected', r.getAttribute('data-file') === id); });
tabs.forEach(function (t) { t.classList.toggle('is-active', t.getAttribute('data-file') === id); });
}
rows.forEach(function (r) { r.addEventListener('click', function () { selectFile(r.getAttribute('data-file')); }); });
tabs.forEach(function (t) { t.addEventListener('click', function () { selectFile(t.getAttribute('data-file')); }); });
selectFile('app');
/* --- terminal run simulation --- */
var termLines = root.querySelector('#vtTermLines');
var runBtn = root.querySelector('#vtRunBtn');
var runLabel = root.querySelector('#vtRunLabel');
var runHint = root.querySelector('#vtRunHint');
var browser = root.querySelector('#vtBrowser');
var placeholder = root.querySelector('.vt-output-placeholder');
var running = false;
var lines = [
{ text: '$ npm start', cls: 'is-cmd', delay: 0 },
{ text: 'Starting the development server\u2026', cls: '', delay: 500 },
{ text: 'Compiled successfully!', cls: 'is-ok', delay: 1350 },
{ text: 'You can now view portal-demo in the browser.', cls: '', delay: 1600 },
{ text: ' Local: http://localhost:3000', cls: '', delay: 1850 }
];
runBtn.addEventListener('click', function () {
if (running) return;
running = true;
runBtn.disabled = true;
runLabel.textContent = 'running\u2026';
browser.classList.remove('is-shown');
placeholder.style.display = 'block';
termLines.innerHTML = '';
lines.forEach(function (line) {
setTimeout(function () {
var div = document.createElement('div');
div.className = 'vt-term-line ' + line.cls;
div.textContent = line.text;
termLines.appendChild(div);
}, line.delay);
});
setTimeout(function () {
placeholder.style.display = 'none';
browser.classList.add('is-shown');
runBtn.disabled = false;
runLabel.textContent = 'npm start';
runHint.textContent = 'server running \u2014 rerun anytime';
running = false;
}, 2300);
});
/* --- resize handles (pointer-capture drag) --- */
function makeResizable(handle, opts) {
var dragging = false, startPos = 0, startSize = 0;
handle.addEventListener('pointerdown', function (e) {
dragging = true;
handle.classList.add('is-dragging');
startPos = opts.axis === 'x' ? e.clientX : e.clientY;
startSize = opts.target.getBoundingClientRect()[opts.axis === 'x' ? 'width' : 'height'];
handle.setPointerCapture(e.pointerId);
e.preventDefault();
});
handle.addEventListener('pointermove', function (e) {
if (!dragging) return;
var pos = opts.axis === 'x' ? e.clientX : e.clientY;
var delta = pos - startPos;
if (opts.invert) delta = -delta;
var size = Math.min(opts.max, Math.max(opts.min, startSize + delta));
opts.target.style[opts.prop] = size + 'px';
if (opts.onResize) opts.onResize(size);
});
function stop() { dragging = false; handle.classList.remove('is-dragging'); }
handle.addEventListener('pointerup', stop);
handle.addEventListener('pointercancel', stop);
}
var sidebar = root.querySelector('.vt-sidebar');
var sidebarHandle = root.querySelector('#vtSidebarHandle');
var lastSidebarWidth = sidebar.getBoundingClientRect().width || 160;
makeResizable(sidebarHandle, {
axis: 'x', target: sidebar, prop: 'width', min: 110, max: 280, invert: false,
onResize: function (s) { lastSidebarWidth = s; }
});
var terminal = root.querySelector('.vt-terminal');
var terminalHandle = root.querySelector('#vtTerminalHandle');
var lastTerminalHeight = terminal.getBoundingClientRect().height || 150;
makeResizable(terminalHandle, {
axis: 'y', target: terminal, prop: 'height', min: 60, max: 280, invert: true,
onResize: function (s) { lastTerminalHeight = s; }
});
var vtBody = root.querySelector('.vt-body');
var windowHandle = root.querySelector('#vtWindowHandle');
makeResizable(windowHandle, {
axis: 'y', target: vtBody, prop: 'height', min: 300, max: 600, invert: false
});
/* --- show/hide toggles --- */
var toggleSidebarBtn = root.querySelector('#vtToggleSidebar');
var sidebarHidden = false;
toggleSidebarBtn.addEventListener('click', function () {
sidebarHidden = !sidebarHidden;
if (sidebarHidden) {
var w = sidebar.getBoundingClientRect().width;
if (w > 0) lastSidebarWidth = w;
sidebar.classList.add('is-hidden');
sidebarHandle.style.display = 'none';
} else {
sidebar.classList.remove('is-hidden');
sidebar.style.width = lastSidebarWidth + 'px';
sidebarHandle.style.display = '';
}
});
var toggleTerminalBtn = root.querySelector('#vtToggleTerminal');
var terminalCollapsed = false;
toggleTerminalBtn.addEventListener('click', function () {
terminalCollapsed = !terminalCollapsed;
if (terminalCollapsed) {
var h = terminal.getBoundingClientRect().height;
if (h > 30) lastTerminalHeight = h;
terminal.classList.add('is-collapsed');
terminalHandle.style.display = 'none';
} else {
terminal.classList.remove('is-collapsed');
terminal.style.height = lastTerminalHeight + 'px';
terminalHandle.style.display = '';
}
});
/* --- real behavior: open/close the "portal" — its DOM node lives outside app-container --- */
var openBtn = root.querySelector('#vtOpenBtn');
var closeBtn = root.querySelector('#vtCloseBtn');
var overlay = root.querySelector('#vtModalOverlay');
openBtn.addEventListener('click', function () {
overlay.classList.add('is-open'); // this node was never inside #vtAppContainer, so overflow:hidden can't touch it
});
closeBtn.addEventListener('click', function () {
overlay.classList.remove('is-open');
});
})();
</script>✏️ Common Use Cases
Use Case | Why Portals? |
|---|---|
Modals / Dialogs | To bypass |
Tooltips / Popovers | To ensure they appear on the top layer without being clipped. |
Global Notifications | To render toasts at a fixed position regardless of where they are triggered. |
Sticky Headers | Sometimes used to move elements to a global "header" area from deep within a page. |