A portal moves an element out of its parent's DOM position, so overlays, dialogs, and tooltips escape overflow: hidden and z-index stacking contexts.
createPortal(children, container?, options?): { element, unmount }| Parameter | Description |
|---|---|
children |
The element (or string) to move |
container |
Where to append it; default document.body |
options.ownerScope |
The owning component's scope element (el.closest('[bf-s]')) |
It returns the mounted element and an unmount() function that removes it from the container.
#Example: Tooltip
Create the portal in a ref callback, once the element exists:
"use client"
import { createSignal, createEffect, createPortal, isSSRPortal } from '@barefootjs/client'
export function Tooltip(props: { text: string; children?: Child }) {
const [visible, setVisible] = createSignal(false)
const handleMount = (el: HTMLElement) => {
if (el.parentNode !== document.body && !isSSRPortal(el)) {
const ownerScope = el.closest('[bf-s]') ?? undefined
createPortal(el, document.body, { ownerScope })
}
createEffect(() => {
el.hidden = !visible()
})
}
return (
<div>
<span
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
{props.children}
</span>
<div className="tooltip" ref={handleMount}>
{props.text}
</div>
</div>
)
}isSSRPortal(el) is true when the server already rendered the element at its portal destination, so the guard skips portaling it twice. ownerScope marks the moved element as belonging to its component, which keeps scoped queries finding it and keeps document.body child order stable (component root first, then its portals in creation order).
#Cleanup
Remove the portal when the component is disposed:
"use client"
import { createPortal, onCleanup } from '@barefootjs/client'
const handleMount = (el: HTMLElement) => {
const portal = createPortal(el, document.body)
onCleanup(() => {
portal.unmount()
})
}To target a container other than document.body, pass it as the second argument: createPortal(el, document.getElementById('modal-root')!).