alpine-portal
v1.0.0
Published
x-portal directive for alpine to moving elements between different points based on the screen size.
Maintainers
Readme
Alpine Portal
About
Alpine Portal adds the x-portal directive to Alpine.js — a bidirectional, screen-size-aware teleportation directive.
Unlike Alpine's built-in x-teleport which moves elements unconditionally, x-portal:
- 📐 Teleports based on breakpoints — move elements to a target container only when a media query matches.
- 🔄 Bidirectional & Order-Preserving — elements automatically return to their exact original sibling position via DOM anchors.
- 🎯 Multi-Target Routing — route elements to different targets across multiple responsive breakpoints.
- 🧩 Placement Modifiers — control target insertion (
.prepend,.append,.before,.after). - 🛡️ Prevents Layout Shift — optional
.spacerkeeps space in the original layout to eliminate Cumulative Layout Shift (CLS). - ⚡ Performant — uses
window.matchMediawith native CSS engine evaluation instead of resize listeners. - 🪶 Lightweight — zero runtime dependencies, under 2KB gzipped.
Think of it as a two-way portal between screen sizes, moving elements back and forth as the viewport changes. 🚀
Installation
CDN
Include the following <script> tag in the <head> of your document, before Alpine:
<script src="https://cdn.jsdelivr.net/gh/nabeghe/[email protected]/dist/alpine-portal.min.js" defer></script>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>NPM
npm install alpine-portalRegister the plugin before starting Alpine:
import Alpine from 'alpinejs';
import Portal from 'alpine-portal';
// Basic registration
Alpine.plugin(Portal);
// Or with custom screen breakpoints
Alpine.plugin(Portal, {
screens: {
tablet: 800,
desktop: 1200,
}
});
window.Alpine = Alpine;
window.Alpine.start();Usage
1. Basic Teleportation
Add x-portal to any element:
<div x-data id="source" style="background: #E91E63; padding: 1rem;">
<h2>Source Container</h2>
<div x-portal="#target" x-portal:screen="640">
I teleport to #target when viewport ≥ 640px!
</div>
</div>
<div x-data id="target" style="background: #8BC34A; padding: 1rem;">
<h2>Target Container</h2>
<!-- Portal content appears here on wide screens -->
</div>Result: When viewport width ≥ 640px, the inner <div> moves to #target. When viewport shrinks below 640px, it returns to its exact original place among its siblings.
2. Named Breakpoints (Tailwind Compatible)
Use standard named breakpoints (sm, md, lg, xl, 2xl):
<!-- Min-width breakpoint: viewport ≥ 768px -->
<div x-portal="#target" x-portal:screen="'md'">...</div>
<!-- Or using modifier syntax -->
<div x-portal="#target" x-portal:screen.md>...</div>
<!-- Max-width breakpoint: viewport ≤ 768px -->
<div x-portal="#drawer" x-portal:screen="'-md'">...</div>Breakpoint Presets:
| Name | Min-Width | Max-Width (-name) |
|------|-----------|----------------------|
| sm | 640px | ≤ 640px |
| md | 768px | ≤ 768px |
| lg | 1024px | ≤ 1024px |
| xl | 1280px | ≤ 1280px |
| 2xl | 1536px | ≤ 1536px |
3. Range & Arbitrary Media Queries
Teleport elements within a specific screen range or custom CSS media query:
<!-- Range syntax: between 640px and 1024px -->
<div x-portal="#tablet-menu" x-portal:screen="'640-1024'">...</div>
<div x-portal="#tablet-menu" x-portal:screen="[640, 1024]">...</div>
<!-- Arbitrary CSS media queries -->
<div x-portal="#landscape-banner" x-portal:media="(orientation: landscape)">...</div>
<div x-portal="#touch-controls" x-portal:media="(pointer: coarse)">...</div>4. Placement Modifiers
Control where the element lands inside or relative to the destination:
<!-- Prepend inside target (first child) -->
<div x-portal.prepend="#target" x-portal:screen="640">...</div>
<!-- Append inside target (default, last child) -->
<div x-portal.append="#target" x-portal:screen="640">...</div>
<!-- Insert immediately before target element -->
<div x-portal.before="#sibling" x-portal:screen="640">...</div>
<!-- Insert immediately after target element -->
<div x-portal.after="#sibling" x-portal:screen="640">...</div>5. Multi-Target Responsive Routing
Move an element to different destinations across different screen sizes:
<div x-portal="{
'-640': '#mobile-drawer',
'641-1024': '#tablet-header',
'1025': '#desktop-sidebar'
}">
Responsive navigation that travels across 3 different containers!
</div>6. Preventing Layout Shift (.spacer)
Leave an invisible placeholder in the element's original position while it is teleported away to eliminate Cumulative Layout Shift (CLS):
<div x-portal.spacer="#target" x-portal:screen="768">
I leave an invisible spacer behind so page content doesn't jump!
</div>7. Conditional Teleportation (x-portal:when)
Combine media queries with dynamic Alpine state variables:
<div x-data="{ isSearchOpen: false }">
<button @click="isSearchOpen = !isSearchOpen">Toggle</button>
<!-- Only teleports if on mobile AND search is open -->
<div x-portal="#overlay" x-portal:screen="-768" x-portal:when="isSearchOpen">
Search bar content
</div>
</div>8. Custom Lifecycle Events
Alpine Portal dispatches custom DOM events when elements teleport or revert:
<div x-portal="#target"
x-portal:screen="640"
@portal:teleport="console.log('Moved to', $event.detail.target)"
@portal:revert="console.log('Returned to original parent')">
...
</div>Event Details:
portal:teleport:{ target, targetElement, placement, portal }portal:revert:{ originalParent, portal }
Directive Reference
| Directive / Modifier | Description |
|----------------------|-------------|
| x-portal="#selector" | Target selector — where the element teleports to |
| x-portal="{ routes }" | Multi-target routing dictionary |
| x-portal.prepend | Prepends inside destination container |
| x-portal.append | Appends inside destination container (default) |
| x-portal.before | Inserts immediately before target element |
| x-portal.after | Inserts immediately after target element |
| x-portal.spacer | Inserts invisible layout spacer when teleported |
| x-portal:screen="val" | Breakpoint (number, 'md', '-md', '640-1024', or expression) |
| x-portal:screen.md | Named breakpoint modifier shortcut |
| x-portal:media="query" | Arbitrary media query string |
| x-portal:when="expr" | Conditional boolean expression |
Examples
The examples/ directory includes several interactive demos you can open in your browser:
| Example | Description | Features |
|---------|-------------|----------|
| Basic | Simple teleportation between two containers | 640px, events |
| Multi-Target Routing | Route an element dynamically across 3 containers | x-portal="{ routes }" |
| Advanced v1.0.0 | Sibling order preservation, .prepend, .spacer, when | Modifiers, CLS, state |
| Responsive Nav | Nav links move from mobile drawer to desktop header | 768px (md) |
| Blog Layout | Sidebar widgets teleport from below article to right sidebar | 900px |
| Product Page | Purchase CTA teleports from below image to sticky sidebar | 900px |
| Dashboard | Stats cards reorganize from inline cards to sidebar summary | 1000px |
Open
examples/index.htmlfor a hub page linking to all demos.
Testing
The project includes a comprehensive test suite using Vitest with jsdom:
npm test # Run all tests once
npm run test:watch # Run in watch mode| Test File | Tests | Coverage |
|-----------|-------|---------|
| tests/Portal.test.js | 24 | Sibling order preservation, breakpoints, ranges, placements, spacer, events, multi-target, destroy, Livewire |
| tests/directive.test.js | 17 | Directive registration, modifiers, named screen, media, when, routes, cleanup hook |
| tests/build.test.js | 2 | alpine:init listener, UMD bundle initialization |
Livewire Support
Alpine Portal is fully compatible with Laravel Livewire's wire:navigate. The plugin automatically cleans up matchMedia event listeners during SPA page transitions to prevent memory leaks.
Changelog
See CHANGELOG.md for full version history.
- v1.0.0 — Stable Release: DOM position preservation via comment anchors, named breakpoints (
sm/md/lg), placement modifiers (.prepend/.before/.after), multi-target routing,.spacerCLS prevention,portal:teleport/portal:revertevents,x-portal:when, and Alpine v3cleanup()hook. - v0.2.4 — Laravel Livewire
wire:navigatefix. - v0.2.0 — Switched to
window.matchMedia. - v0.1.0 — Initial release.
📜 License
Created with ❤️ by Nabeghe. Licensed under the MIT License.
