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