Viewport portal & target
Introduction
ViewportPortal and ViewportTarget are utility components that allow a single DOM fragment to be rendered in different places depending on the current breakpoint. They are particularly useful for complex layouts—e.g., rendering cart totals inside a sidebar on desktop and below the product list on mobile—without duplicating markup or re-fetching data.
The pair relies on Alpine 2 for reactivity and works with Livewire out of the box.
Components
| Component | Purpose |
|---|---|
<x-ui.viewport-portal> | Declares the source of the markup. It should appear once. |
<x-ui.viewport-target> | Declares one or more destinations where the portal content should be injected when the viewport rule matches. |
<x-ui.viewport-portal>
| Prop | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Logical identifier used to pair the portal with its targets. |
<x-ui.viewport-portal name="cart-totals">
@livewire('cart.totals')
</x-ui.viewport-portal>
<x-ui.viewport-target>
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Must match the name used by the portal. |
viewport | string | both | Breakpoint rule that must evaluate to true for the content to appear. |
Accepted viewport values:
| Value | Description |
|---|---|
mobile | < md (< 768 px) |
desktop | ≥ md (≥ 768 px) |
sm, md, lg, … | Tailwind's standard breakpoints. |
max-lg | Smaller than the named breakpoint. |
both | Always matches (default). |
{{-- Mobile (<768 px) */}
<x-ui.viewport-target name="cart-totals" viewport="mobile" />
{{-- Desktop (≥768 px) */}
<x-ui.viewport-target name="cart-totals" viewport="desktop" />
How it works
- The portal registers itself globally via JavaScript (
viewportPortalComponent). - Each target subscribes to viewport changes (using
matchMedia). - When the rule becomes
true, the target appends a cloned version of the portal content into its own node. When the rule becomesfalse, the cloned node is removed. - Because the same DOM subtree is reused, Livewire keeps its internal state intact—no re-render or network request is triggered.
Migration note
When the codebase upgrades to Alpine 3, the recommended helper function name changes from viewportPortalComponent(...) to viewportPortal(...). No template modifications are necessary beyond that rename.
Example – cart totals
{{-- 1️⃣ Declare the portal once */}
<x-ui.viewport-portal name="cart-totals">
@livewire('cart.totals')
</x-ui.viewport-portal>
{{-- 2️⃣ Place targets wherever needed */}
<x-ui.viewport-target name="cart-totals" viewport="mobile" />
<x-ui.viewport-target name="cart-totals" viewport="desktop" />
Related files
resources/views/components/ui/viewport-portal.blade.phpresources/views/components/ui/viewport-target.blade.php- Alpine helper:
resources/js/alpine-components/viewport-portal-component.js
Updated for Laravel 10, Livewire 2, Alpine 2, and Tailwind 3.