Portal mode
Moving a component's root into document.body while it is open, so it can't be clipped by overflow: hidden ancestors or trapped in a stacking context — then putting it back exactly where it was.
An anchored panel rendered inside a scrollable card or a transformed ancestor inherits its constraints: it gets cut off, or stacks under elements it should cover. Portal mode side-steps both by re-parenting the root to <body> for the time it is open. The popover uses it; modal is the other natural candidate.
On open
Before moving the root, the component drops a placeholder comment node in its place — that comment is the way home:
function open(trigger) {
/* Portal mode: move the popover into <body> to escape
overflow:hidden clipping and stacking-context issues */
placeholder = document.createComment('ng-popover-portal');
root.parentNode.insertBefore(placeholder, root);
document.body.appendChild(root);
// ...position, is-open, events...
}
On close
The root is re-inserted before the placeholder, and the placeholder is removed. The root must never be left orphaned in <body>:
function close() {
// ...is-open off, focus back to trigger...
/* Restore the original DOM position */
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.insertBefore(root, placeholder);
placeholder.remove();
placeholder = null;
}
}
Interaction with the observer
To a MutationObserver, a re-parented node looks like a removal from its old parent — and the observer's job is to unmount removed components. The guard: a node still connected to the document was moved, not removed, so the unmount is skipped:
// ng_observer.js — _flushRemoved()
if (document.contains(removed)) return; // moved, not removed → skip unmount
Any new component adopting portal mode must be checked against this path — see Component lifecycle.
Rules of thumb
| When | Only when clipping or stacking is a real risk (anchored overlays). Not a default for every component. |
|---|---|
| Placeholder | Always a comment node in the original position; restore on close, remove the placeholder. |
| Cleanup | Inline positioning styles set while open are reset on close. |
| Observer | Re-parenting must stay invisible to the unmount path (moved ≠ removed). |
Related
ng-popover is-open ng:popover:open ng:popover:close document.contains()