A Portal
Sometimes a component needs to render something that visually breaks out of
where it sits — a dialog, a tooltip, a dropdown. The obstacle is CSS: if any
ancestor has overflow: hidden, a position, or a z-index stacking
context, your dialog gets clipped or trapped beneath other content. The
classic fix is a portal: render the content inside your component, but
place its DOM somewhere else — usually document.body, where nothing clips
it.
Marko 6 doesn’t ship a portal tag, but you can build one in a few lines. The shape is already here:
<div/portalEl> <${input.content}/></div>
<script> /* move portalEl() somewhere else */</script><${input.content}/> renders whatever the caller passed between the tags, and
<div/portalEl> hands you the real DOM node wrapping it.
Move it (your job)
Open src/tags/portal.marko and finish the <script>. Grab the wrapper node,
pick the target (an element by id via input.target, or document.body by
default), move the node there, and clean up when the portal unmounts:
<script> const el = portalEl(); const target = input.target ? document.getElementById(input.target) : document.body; target.appendChild(el); $signal.onabort = () => el.remove();</script>Now open the dialog. It’s declared inside the clipped card, yet it appears
over the whole page — because appendChild relocated its DOM to
document.body, outside the card’s overflow: hidden. Marko still owns the
content: the Close button keeps working, because moving a node with
appendChild doesn’t change the node, only where it lives. And
$signal.onabort removes it the moment the portal unmounts (here, when the
dialog closes), so nothing is left orphaned in the body.
That’s a portal: render content in one place, relocate its DOM to another, and
clean up after itself — an escape hatch from CSS clipping, built from a node
handle, appendChild, and $signal.
- Installing dependencies
- Starting dev server