Alpine.js Plugins Worth Using in a Laravel Project

The problem: Alpine's plugin list (Focus, Collapse, Persist, Intersect, Anchor, Mask, Morph) looks appealing, but installing all of them "just in case" bloats the JS bundle for a Laravel app that might only ever need one or two — and some overlap with things Livewire already handles.

Environment: Laravel 11, Alpine.js 3.x (Livewire-bundled), Vite 5, npm.

The Error

// Not a runtime error — a bundle-size/redundancy problem.
// Symptom: checking `npm run build` output and seeing
// app.js grow noticeably after adding plugins you're
// not actually using directives from.

vite v5.x building for production...
✓ 142 modules transformed.
public/build/assets/app-a1b2c3d4.js   187.42 kB │ gzip: 61.20 kB
// (up from ~140 kB before adding three unused plugins)

Why It Happens

Each official Alpine plugin is its own small package that adds new directives/magics on top of core Alpine. They're not tree-shaken automatically just by being imported — if you import and register a plugin via Alpine.plugin(...), its code ships in your bundle whether or not you end up using its directives on a given page. In a Laravel + Livewire app specifically, a few of these plugins also solve problems Livewire itself already solves server-side, which makes them redundant rather than complementary.

The Fix

Install only the plugins that solve something core Alpine + Livewire genuinely can't, and register them via alpine:init so they attach to Livewire's Alpine instance:

@alpinejs/collapse — worth it. Smooth expand/collapse transitions (accordions, "show more" sections) that would otherwise need manual CSS max-height hacks:

// npm install @alpinejs/collapse

// resources/js/app.js
import collapse from '@alpinejs/collapse';

document.addEventListener('alpine:init', () => {
    Alpine.plugin(collapse);
});
<div x-data="{ open: false }">
    <button @click="open = !open">Toggle</button>
    <div x-show="open" x-collapse>
        Long content that expands/collapses smoothly...
    </div>
</div>

@alpinejs/focus — worth it for modals/dropdowns. Handles focus trapping (keeping Tab navigation inside an open modal) correctly, which is fiddly to get right manually and matters for accessibility:

// npm install @alpinejs/focus

import focus from '@alpinejs/focus';
Alpine.plugin(focus);
<div x-show="modalOpen" x-trap="modalOpen">
    <input type="text">
    <button @click="modalOpen = false">Close</button>
</div>

@alpinejs/persist — situational. Persists Alpine state to localStorage across page loads (e.g. remembering a collapsed sidebar). Skip this if the same preference should also survive across devices/sessions server-side — in that case store it as a user preference in your database via Livewire/a controller instead, since persist only lives in that one browser.

<div x-data="{ sidebarOpen: $persist(true) }">
    <!-- fine for pure UI preference, not for anything
         that should sync across the user's devices -->
</div>

@alpinejs/intersect — situational. Useful for lazy-loading or reveal-on-scroll animations, but if you're paginating a long list, prefer Livewire's wire:scroll-style infinite scroll patterns or standard pagination over rebuilding it with Intersect + client-side fetch, since Livewire already has server-aware primitives for that.

@alpinejs/morph / @alpinejs/anchor — usually skip in a Livewire project. Morph overlaps heavily with what Livewire already does for DOM diffing on updates, and Anchor (positioning floating elements) is only worth adding if you're not already using a Tailwind UI kit or headless library that handles positioning for you.

Step-by-Step

  1. List every Alpine directive/magic your project actually uses in templates (x-collapse, x-trap, $persist, etc.) by searching your Blade files.
  2. Install only the plugin packages matching directives you found in step 1 — npm install @alpinejs/collapse @alpinejs/focus etc.
  3. Register each installed plugin inside document.addEventListener('alpine:init', () => { Alpine.plugin(x) }) in app.js.
  4. Rebuild with npm run build and compare the output bundle size against your previous build.
  5. For anything Livewire already solves server-side (persisted preferences, scroll-based pagination, DOM morphing), prefer the Livewire-native approach over adding an Alpine plugin for the same job.
  6. Remove any plugin import that isn't backed by an actual directive used somewhere in your Blade views.
Edge case: @alpinejs/persist stores data in localStorage, which is per-browser, not per-user-account — if a user switches browsers or clears site data, persisted state silently resets to defaults with no error. Never use $persist for anything the user would consider "their settings" in a meaningful sense; reserve it for cosmetic, low-stakes UI state only.

Not sure which plugin (if any) actually fits your use case? Ask on the Contact page and describe what you're building.

Advertisement
Advertisement