npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

alpine-portal

v1.0.0

Published

x-portal directive for alpine to moving elements between different points based on the screen size.

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 .spacer keeps space in the original layout to eliminate Cumulative Layout Shift (CLS).
  • Performant — uses window.matchMedia with 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-portal

Register 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.html for 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, .spacer CLS prevention, portal:teleport/portal:revert events, x-portal:when, and Alpine v3 cleanup() hook.
  • v0.2.4 — Laravel Livewire wire:navigate fix.
  • v0.2.0 — Switched to window.matchMedia.
  • v0.1.0 — Initial release.

📜 License

Created with ❤️ by Nabeghe. Licensed under the MIT License.