Skip to main content

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

ComponentPurpose
<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>

PropTypeRequiredDescription
namestringLogical 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>

PropTypeDefaultDescription
namestringMust match the name used by the portal.
viewportstringbothBreakpoint rule that must evaluate to true for the content to appear.

Accepted viewport values:

ValueDescription
mobile< md (< 768 px)
desktop≥ md (≥ 768 px)
sm, md, lg, …Tailwind's standard breakpoints.
max-lgSmaller than the named breakpoint.
bothAlways 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

  1. The portal registers itself globally via JavaScript (viewportPortalComponent).
  2. Each target subscribes to viewport changes (using matchMedia).
  3. When the rule becomes true, the target appends a cloned version of the portal content into its own node. When the rule becomes false, the cloned node is removed.
  4. 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" />
  • resources/views/components/ui/viewport-portal.blade.php
  • resources/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.

X

Graph View