Portals render elements outside their parent DOM hierarchy — useful for overlays, modals, and tooltips that need to escape overflow: hidden or z-index stacking contexts.
import { createPortal } from '@barefootjs/client'#`createPortal`
Moves an element to a different DOM container.
createPortal(children, container?, options?)Type:
type Portal = {
element: HTMLElement
unmount: () => void
}
interface PortalOptions {
ownerScope?: Element // Component scope for scoped queries
}
function createPortal(
children: HTMLElement | string,
container?: HTMLElement, // Default: document.body
options?: PortalOptions
): PortalReturns a Portal object with:
element— the mounted DOM elementunmount()— removes the element from the container
#Basic Usage
Create portals inside ref callbacks:
"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) => {
// Move to document.body to avoid overflow/z-index issues
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>
)
}#SSR Portal Detection
isSSRPortal checks whether an element was already portaled during SSR to prevent double-portaling:
import { isSSRPortal } from '@barefootjs/client'
const handleMount = (el: HTMLElement) => {
// Skip if already portaled during SSR
if (el.parentNode !== document.body && !isSSRPortal(el)) {
createPortal(el, document.body)
}
}After hydration, remove SSR placeholders:
import { cleanupPortalPlaceholder } from '@barefootjs/client'
cleanupPortalPlaceholder(portalId)#Owner Scope
A portaled element is outside its original component's scope, so find() cannot locate it. The ownerScope option links it back:
const handleMount = (el: HTMLElement) => {
const ownerScope = el.closest('[bf-s]') ?? undefined
createPortal(el, document.body, { ownerScope })
}bf-po on the moved element lets scoped queries from the owner still find it.
#Dialog Example
Dialog overlays are a common portal use case:
"use client"
import { createPortal, isSSRPortal, useContext, createEffect } from '@barefootjs/client'
function DialogOverlay() {
const handleMount = (el: HTMLElement) => {
// Portal to body
if (el.parentNode !== document.body && !isSSRPortal(el)) {
const ownerScope = el.closest('[bf-s]') ?? undefined
createPortal(el, document.body, { ownerScope })
}
const ctx = useContext(DialogContext)
// Reactive visibility
createEffect(() => {
const isOpen = ctx.open()
el.dataset.state = isOpen ? 'open' : 'closed'
el.className = isOpen ? 'overlay overlay-visible' : 'overlay overlay-hidden'
})
// Click overlay to close
el.addEventListener('click', () => {
ctx.onOpenChange(false)
})
}
return <div data-slot="dialog-overlay" ref={handleMount} />
}The overlay accesses DialogContext from the component tree but is moved to document.body to escape CSS containment.
#Cleanup
Combine portal.unmount() with onCleanup:
import { createPortal, onCleanup } from '@barefootjs/client'
const handleMount = (el: HTMLElement) => {
const portal = createPortal(el, document.body)
onCleanup(() => {
portal.unmount()
})
}#Custom Container
Specify a different container instead of the default document.body:
const container = document.getElementById('modal-root')!
createPortal(el, container)