<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.
| name | type | description |
|---|---|---|
mount |
Element |
Element to portal into. The element should be in the document. |
children? |
any | content to project into mount |
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)
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)
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)