Portal

A component for rendering children into a different root container.

Syntax

<Portal root={container}>{children}</Portal>

Description

Portal allows you to render children into a DOM node that exists outside the parent component's DOM hierarchy. This is useful for:

When you use renderer.render(), the children are implicitly wrapped in a Portal element with the root set to the second argument.

Portal elements are opaque to their parents - they return undefined as their element value, so parent arrange operations don't see the portal's children.

Props

PropTypeDescription
rootobjectThe container to render children into
childrenChildrenThe elements to render into the root

Examples

Basic modal

import {Portal} from "@b9g/crank";

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

return (
<Portal root={document.body}>
<div class="modal-overlay">
<div class="modal-content">
{children}
</div>
</div>
</Portal>
);
}

function App() {
return (
<div class="app">
<Modal isOpen={true}>
<h2>Modal Title</h2>
<p>This renders at document.body level</p>
</Modal>
</div>
);
}

Tooltip escaping overflow

import {Portal} from "@b9g/crank";

function* Tooltip({children, content}) {
let isVisible = false;
let position = {x: 0, y: 0};

const show = (e) => {
isVisible = true;
position = {x: e.clientX, y: e.clientY};
this.refresh();
};

const hide = () => {
isVisible = false;
this.refresh();
};

for (const {children, content} of this) {
yield (
<span onmouseenter={show} onmouseleave={hide}>
{children}
{isVisible && (
<Portal root={document.body}>
<div
class="tooltip"
style={`position: fixed; left: ${position.x}px; top: ${position.y}px;`}
>
{content}
</div>
</Portal>
)}
</span>
);
}
}

Multiple roots

import {Portal} from "@b9g/crank";

function* MultiRootApp() {
const sidebar = document.getElementById("sidebar");
const main = document.getElementById("main");

for (const props of this) {
yield (
<>
<Portal root={sidebar}>
<Navigation />
</Portal>
<Portal root={main}>
<Content />
</Portal>
</>
);
}
}

Event propagation

Events dispatched on elements inside a Portal still propagate through the Crank component tree, not the DOM tree:

function App() {
const handleClick = () => console.log("Caught in App!");

return (
<div onclick={handleClick}>
<Portal root={document.body}>
{/* Click events here will still trigger handleClick */}
<button>Click me</button>
</Portal>
</div>
);
}

See also

Edit on GitHub