Portal
Renders children into a different part of the DOM tree (a body-level container by default). Useful for tooltips, dropdowns, and modals that need to escape overflow or z-index stacking contexts.
Basic Portal
Toggle the portal on and off. When enabled, the content is teleported to a
container appended to <body>, escaping any overflow or
z-index constraints of its parent. Inspect the DOM to see the element move.
This box has overflow: hidden.
I'm portalled content!
✓ I'm rendered at the end of <body>, escaping the overflow box.
@* Requires <BitBhPortalOutlet /> in your root layout *@
<BitBhButton OnClick="() => enabled = !enabled" class="btn">
@(enabled ? "Disable portal" : "Enable portal")
</BitBhButton>
<div style="overflow: hidden; height: 120px;">
<BitBhPortal Enabled="enabled">
<div class="my-content">
I'm portalled to the outlet at the end of the layout!
</div>
</BitBhPortal>
</div>
@code {
private bool enabled = true;
}.portal-overflow-box {
overflow: hidden;
height: 8rem;
border: 2px dashed #d4d4d8;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
position: relative;
}
.portal-content {
background: #eff6ff;
border: 1px solid #2563eb;
border-radius: 0.4rem;
padding: 0.75rem 1rem;
font-size: 0.9rem;
}Tooltip escaping stacking context
A common use case: a tooltip that needs to escape a parent with
overflow: hidden or a restrictive z-index.
The portal moves the tooltip to body level so it's always visible.
This container has overflow: hidden and z-index: 1.
@* Requires <BitBhPortalOutlet /> in your root layout *@
<div style="overflow: hidden; position: relative; z-index: 1;">
<div @onmouseenter="() => show = true"
@onmouseleave="() => show = false">
Hover me
</div>
@if (show)
{
<BitBhPortal>
<div class="tooltip">
This tooltip escapes the stacking context!
</div>
</BitBhPortal>
}
</div>
@code {
private bool show;
}.portal-tooltip {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #18181b;
color: #fff;
padding: 0.5rem 1rem;
border-radius: 0.4rem;
font-size: 0.85rem;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}Portal Group (shared target)
Use BitBhPortalGroup to direct all nested portals into a shared
container element instead of the default body-level root. This is useful
when you want portalled content to land in a specific region of your layout.
Portal Group target container (portals land here):
<BitBhPortalGroup class="my-portal-target">
<p>Portals land inside this group element:</p>
@if (showA)
{
<BitBhPortal>
<div>Portal A content</div>
</BitBhPortal>
}
@if (showB)
{
<BitBhPortal>
<div>Portal B content</div>
</BitBhPortal>
}
</BitBhPortalGroup>
@code {
private bool showA;
private bool showB;
}.portal-group-target {
border: 2px solid #a78bfa;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
min-height: 6rem;
background: #faf5ff;
}
.portal-group-item {
padding: 0.5rem 0.75rem;
border-radius: 0.4rem;
margin-top: 0.5rem;
font-size: 0.9rem;
}
.portal-group-item-a { background: #dbeafe; border: 1px solid #3b82f6; }
.portal-group-item-b { background: #dcfce7; border: 1px solid #22c55e; }Disabled portal (render in place)
Set Enabled="false" to render content in its natural DOM
position without teleportation. This is useful for conditional portalling
based on viewport size or other runtime conditions.
Container with overflow: hidden.
Rendered in place.
<BitBhPortal Enabled="portalEnabled">
<div class="my-content">
@(portalEnabled ? "Portalled to body!" : "Rendered in place.")
</div>
</BitBhPortal>
@code {
private bool portalEnabled;
}.portal-overflow-box {
overflow: hidden;
height: 8rem;
border: 2px dashed #d4d4d8;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
position: relative;
}
.portal-content {
background: #eff6ff;
border: 1px solid #2563eb;
border-radius: 0.4rem;
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
.portal-toggle-label {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
cursor: pointer;
}
.portal-content-alt {
background: #fef3c7;
border-color: #f59e0b;
}