<Portal/>

Inserts its children into a different element while keeping them inside the current reactive scope — no wrapper node is added. For portaling into document.head, prefer <Head/>, which also deduplicates title / meta / rel=canonical.

Portaled content is rendered via insert and is owned by the surrounding component: if that component is disposed, the portaled nodes are removed too.

Attributes

name type description
mount Element Element to portal into. The element should be in the document.
children? any content to project into mount

Examples

Modal in an overlay

Renders its children into a host element instead of the surrounding tree, while keeping context, ownership, and cleanup tied to the component that declared it. Useful for tooltips, modals, and toasts — anything that needs to escape the parent's overflow / z-index context.

import { render, signal } from 'pota'
import { Portal, Show } from 'pota/components'

const overlay = document.createElement('div')
overlay.id = 'overlay'
document.body.append(overlay)

function App() {
	const open = signal(false)

	return (
		<div>
			<button on:click={() => open.update(o => !o)}>
				toggle modal
			</button>
			<Show when={open.read}>
				<Portal mount={overlay}>
					<div class="modal">
						<p>I'm rendered into #overlay.</p>
						<button on:click={() => open.write(false)}>close</button>
					</div>
				</Portal>
			</Show>
		</div>
	)
}

render(App)

Toast queue

A toast container needs to escape any parent's overflow: hidden or stacking context — <Portal/> projects each toast into a global host element while keeping its lifecycle tied to the caller.

import { render, signal } from 'pota'
import { For, Portal } from 'pota/components'

const host = document.createElement('div')
host.id = 'toasts'
host.style.cssText =
	'position:fixed;top:1rem;right:1rem;display:grid;gap:.5rem;z-index:9999'
document.body.append(host)

const toasts = signal([])
let nextId = 0

function notify(text) {
	const id = nextId++
	toasts.update(t => [...t, { id, text }])
	setTimeout(
		() => toasts.update(t => t.filter(x => x.id !== id)),
		2500,
	)
}

function App() {
	return (
		<div>
			<button on:click={() => notify('saved')}>saved</button>
			<button on:click={() => notify('uploaded')}>uploaded</button>

			<Portal mount={host}>
				<For each={toasts.read}>
					{t => (
						<div style="background:#222;color:#fff;padding:.5rem 1rem;border-radius:.25rem">
							{t.text}
						</div>
					)}
				</For>
			</Portal>
		</div>
	)
}

render(App)

Portaling children without a wrapper

Portals can move text, elements, and nested components — without adding any wrapper node. The siblings declared outside the portal stay in place.

import { render } from 'pota'
import { Portal } from 'pota/components'

function Test() {
	return ' me too!'
}

function Example() {
	return (
		<section class="escaping-this-parent">
			<Portal mount={document.body}>
				Portals can move text, elements and include their children.
				<br />
				Without any kind of wrapper.
				<Test />
			</Portal>
			I stay here
		</section>
	)
}

render(Example)