Portals
pp.portal(ref, target?) renders an element somewhere else in the document — by default under document.body — while its state, bindings and event handlers keep living in the component that owns it.
Why
Dialogs, dropdowns and toasts break when an ancestor has overflow: hidden,
a transform, a filter or a stacking context: the overlay clips or stacks under
later content. A portal escapes the ancestor chain physically while staying
logically inside its component.
A portaled dialog
<div pp-component="confirm_delete">
<button onclick="setOpen(true)">Delete account</button>
<div pp-ref="dialogRef" hidden="{!open}" class="modal-backdrop">
<div class="modal">
<p>This cannot be undone. Continue?</p>
<button onclick="confirm()">Yes, delete</button>
<button onclick="setOpen(false)">Cancel</button>
</div>
</div>
<script>
const [open, setOpen] = pp.state(false);
const dialogRef = pp.ref(null);
// Moves the ref'd element under document.body while this component
// keeps owning its state, bindings and events.
pp.portal(dialogRef);
const confirm = async () => {
await pp.rpc("deleteAccount");
setOpen(false);
};
</script>
</div>
- The element is referenced with
pp-ref, so the component keeps a handle after the move. - Bindings like
hidden="{!open}"and handlers keep working — scope follows the component, not the DOM position. - The returned object includes
sourceParentif you need where it came from.
Choosing a target
<!-- Portal into a specific host instead of document.body -->
<script>
const toastRef = pp.ref(null);
pp.portal(toastRef, document.getElementById("toast-region"));
</script>
Rules
- Portal a ref-managed element, not arbitrary markup strings.
- Toggle visibility with
hidden="{...}"state — do not add/remove the element manually. - The portaled subtree is still disposed with its owning component; no manual teardown needed.