Portals CORE+
Render content to a different location in the DOM while maintaining component ownership and reactivity.
Why Use Portals?
Sometimes the visual location of content needs to differ from its logical location in the component hierarchy. Common use cases include:
Modals & Dialogs
Render at <body> level to escape overflow: hidden containers and z-index stacking contexts.
Tooltips & Popovers
Position tooltips outside scrollable containers so they don't get clipped.
Dropdown Menus
Render menus outside their parent container to avoid overflow issues.
Basic Usage
Add data-portal to any element with a CSS selector pointing to the target location:
<!-- Portal target (content teleports here) -->
<div id="notification-area" class="mb-3"
style="min-height: 50px; border: 2px dashed #ccc; padding: 10px; border-radius: 4px;">
<small class="text-muted">Portal target area</small>
</div>
<!-- Component with portal -->
<div data-component="portal-demo">
<button class="btn btn-primary" data-action="toggleNotification">
Toggle Notification
</button>
<span class="ms-2" data-bind="isShowing ? 'Notification visible' : 'Notification hidden'"></span>
<!-- This content teleports to #notification-area -->
<div data-portal="#notification-area" data-show="isShowing">
<div class="alert alert-success mb-0">
<strong>Success!</strong> This notification is portaled above!
</div>
</div>
</div>
wildflower.component('portal-demo', {
state: {
isShowing: false
},
toggleNotification() {
this.isShowing = !this.isShowing
}
})
Notice how the notification appears in the "Portal target area" above the button, even though it's defined inside the component below the button. This is the power of portals!
Portal Targets
The data-portal attribute accepts any valid CSS selector:
<!-- By ID -->
<div data-portal="#modal-container">...</div>
<!-- By class -->
<div data-portal=".tooltip-layer">...</div>
<!-- To body -->
<div data-portal="body">...</div>
<!-- Complex selector -->
<div data-portal="#app .overlay-container">...</div>
Portals with Conditional Rendering
Portals work seamlessly with data-show and data-render:
With data-show
The content is teleported but visibility is toggled via display: none:
<div data-portal="#tooltip-layer" data-show="showTooltip">
<div class="tooltip">
<span data-bind="tooltipText"></span>
</div>
</div>
With data-render
The content is only teleported when the condition is true, and removed from the target when false:
<div data-portal="#notification-area" data-render="hasNotification">
<div class="notification">
<span data-bind="notificationMessage"></span>
</div>
</div>
Reactivity in Portals
Portaled content maintains full reactivity with its source component:
<!-- Portal target -->
<div id="counter-display" class="mb-3 p-3 bg-info text-white rounded">
<small>Portaled content appears here:</small>
</div>
<!-- Component with portal -->
<div data-component="counter-portal-demo">
<div class="card">
<div class="card-body">
<h6>Source Component</h6>
<p>Count: <strong data-bind="count"></strong></p>
<button class="btn btn-primary btn-sm" data-action="increment">+1 Here</button>
</div>
</div>
<!-- This is portaled to #counter-display above -->
<div data-portal="#counter-display">
<div class="d-flex align-items-center gap-2">
<span>Count: <strong data-bind="count"></strong></span>
<button class="btn btn-light btn-sm" data-action="increment">+1 From Portal</button>
</div>
</div>
</div>
wildflower.component('counter-portal-demo', {
state: { count: 0 },
increment() {
this.count++
// Both displays update - in source AND in portal!
}
})
Click either button - both counters update because they share the same component state!
Actions in Portals
Event handlers (data-action) in portaled content call methods on the source component:
<div data-component="modal-actions-demo">
<button data-action="open">Open Modal</button>
<div data-portal="body" data-show="isOpen">
<div class="modal">
<!-- These actions call methods on modal-actions-demo -->
<button data-action="save">Save</button>
<button data-action="close">Cancel</button>
</div>
</div>
</div>
wildflower.component('modal-actions-demo', {
state: { isOpen: false },
open() { this.isOpen = true },
close() { this.isOpen = false },
save() {
console.log('Save clicked from portal!')
this.isOpen = false
}
})
Portals in Lists
Portals work inside data-list iterations. Each list item can have its own portal:
<div data-component="list-with-portals">
<ul data-list="items">
<template>
<li>
<span data-bind="name"></span>
<button data-action="showDetails">Details</button>
<!-- Each item can portal its own tooltip -->
<div data-portal="#tooltip-layer" data-show="showTooltip">
<div class="tooltip" data-bind="description"></div>
</div>
</li>
</template>
</ul>
</div>
Cleanup
Portaled content is automatically cleaned up when:
- The source component is destroyed
- A
data-rendercondition becomes false - The portal element is removed from the DOM
The framework tracks all portaled content and ensures proper cleanup to prevent memory leaks.
Best Practices
Do
- Use portals for modals, tooltips, and dropdowns
- Ensure portal targets exist before components render
- Combine with
data-showordata-renderfor conditional display - Keep portal content simple when possible
Don't
- Use portals when regular positioning would work
- Create deeply nested portal chains
- Forget to handle keyboard accessibility (focus management)
- Assume portal content will be in a specific DOM order
Example: Tooltip System
Here's a complete tooltip implementation using portals:
<!-- Tooltip layer at body level -->
<div id="tooltip-layer" style="position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9999;"></div>
<!-- Component with tooltip -->
<div data-component="tooltip-demo">
<button
data-action="mouseenter:showTip mouseout:hideTip"
class="btn btn-primary">
Hover me
</button>
<div data-portal="#tooltip-layer" data-show="showTooltip">
<div class="tooltip"
data-bind-style="tooltipStyle">
This is a tooltip!
</div>
</div>
</div>
wildflower.component('tooltip-demo', {
state: {
showTooltip: false,
tooltipStyle: {}
},
showTip(event) {
const rect = event.target.getBoundingClientRect()
this.tooltipStyle = {
position: 'absolute',
left: rect.left + 'px',
top: (rect.bottom + 5) + 'px'
}
this.showTooltip = true
},
hideTip() {
this.showTooltip = false
}
})
Simple Syntax
Portals use a single attribute - no wrapper components, no JavaScript API calls:
<!-- Content renders inside #modal-container, not here -->
<div data-portal="#modal-container">
<div class="modal">I appear elsewhere in the DOM</div>
</div>
The element stays in your component's HTML for easy reading, but renders at the target location. All bindings and reactivity work normally.