Alpine.js con Laravel 13: interactividad ligera sin salir de Blade
Alpine.js pesa unos 15 KB, se escribe con atributos dentro de tu HTML de Blade y convierte cualquier vista de Laravel 13 en una interfaz interactiva sin compilar nada. Si necesitas un desplegable, un modal o un filtro, no hace falta una SPA: este tutorial recorre el flujo completo, de la instalación al fetch contra tus propias rutas.
Qué es Alpine.js y cuándo usarlo en Laravel
Alpine.js es un framework declarativo de JavaScript que añade comportamiento reactivo directamente en el markup: directivas como x-data, x-show, x-for o x-model controlan el estado y el DOM sin paso de build ni bundler. Se distribuye como un único archivo de unos 15 KB en minificado con gzip (alrededor de 17 KB sin comprimir), así que apenas afecta al peso de la página. Nació como la alternativa ligera a frameworks completos como Vue o React para proyectos donde el backend ya resuelve la mayor parte del trabajo con plantillas servidor.
Alpine vs Livewire vs HTMX: dónde encaja cada uno
En el ecosistema Laravel conviven varias formas de añadir interactividad. Livewire 4 mantiene el estado en el servidor y re-renderiza componentes con peticiones AJAX; HTMX 2 se centra en intercambiar fragmentos de HTML con el servidor mediante atributos como hx-get o hx-post; Alpine.js, en cambio, vive enteramente en el cliente: el estado, los eventos y el renderizado ocurren en el navegador. La regla práctica: si necesitas lógica de servidor (persistencia, permisos), usa Livewire o HTMX; si lo que quieres es interacción pura de interfaz, Alpine es más simple y rápido. De hecho, Livewire lo incluye como dependencia para la interactividad instantánea del lado del cliente.
El caso ideal: vistas server-rendered con toques de interactividad
El caso de uso perfecto es una vista Blade clásica a la que solo le faltan comportamientos pequeños: un menú móvil, un acordeón, un contador, un filtro en una tabla. Ese es exactamente el punto donde una SPA añadiría complejidad sin retorno, y donde Alpine rinde al máximo: cero compilación, cero estado de servidor y el HTML como única fuente de verdad.
Instalación en Laravel 13: CDN o Vite
Con Vite y npm: npm install alpinejs
En un proyecto Laravel 13 con Vite, la instalación es un comando:
npm install alpinejsDespués se importa y se registra en resources/js/app.js:
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();Y se compila con npm run build. Como Blade y Vite comparten el mismo manifiesto, la directiva @vite de tu layout cargará el bundle en todas las vistas.
El fallback del CDN para proyectos pequeños
Si no quieres tocar el pipeline de assets, el CDN oficial funciona en proyectos pequeños: una etiqueta script con el atributo defer antes de cerrar body. En ese modo Alpine se autoinicia, sin llamar a Alpine.start(). El enfoque CDN es cómodo para prototipos, pero en producción Vite permite versionar, minificar y servir los assets desde tu propio dominio.
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>Primeros componentes: x-data, x-show y x-on
Desplegables y menús con un solo atributo
Todo componente Alpine empieza con x-data, que define el estado local, normalmente un objeto literal de JavaScript. El ejemplo clásico, un desplegable:
<div x-data="{ open: false }">
<button @click="open = !open">Menú</button>
<div x-show="open">
<a href="#">Opción 1</a>
<a href="#">Opción 2</a>
</div>
</div>x-show alterna la visibilidad del elemento (display: none) según el valor de open, y x-on (con el atajo @) escucha eventos del DOM. Nada de JavaScript aparte: la lógica vive en el atributo.
Eventos con x-on:click y atajos @click
La directiva x-on admite cualquier evento del navegador: @click, @mouseenter, @keydown.escape, @submit.prevent. El modificador .prevent evita el comportamiento por defecto del formulario sin escribir un listener adicional. Para acciones con más lógica, se extrae un método al objeto de estado:
<div x-data="{ count: 0, increment() { this.count++ } }">
<button @click="increment">+1</button>
<span x-text="count"></span>
</div>Bucles y listas con x-for y x-model
Pintar colecciones de Blade en el cliente
Cuando los datos ya llegan renderizados desde Blade, no hace falta tocarlos. Pero si quieres pintar una colección en el cliente, x-for itera sobre un array del estado. Recuerda que x-for exige una key única en cada iteración:
<ul x-data="{ items: ['Laravel', 'Blade', 'Alpine'] }">
<template x-for="item in items" :key="item">
<li x-text="item"></li>
</template>
</ul>En una vista Blade es habitual inicializar el estado con datos del servidor mediante @json, de modo que PHP y Alpine comparten la misma fuente:
<div x-data="{ projects: @json($projects) }">...</div>Formularios reactivos con x-model
x-model enlaza el valor de un input con el estado en ambas direcciones: al escribir, el estado se actualiza; al cambiar el estado, el input se actualiza. Es ideal para filtros en vivo o para previsualizar datos antes de enviarlos.
<div x-data="{ title: '' }">
<input type="text" x-model="title" placeholder="Título del post">
<p x-show="title.length > 0">Vas a escribir: <strong x-text="title"></strong></p>
</div>Transiciones y animaciones con x-transition
Las transiciones son uno de los mayores atractivos de Alpine. La directiva x-transition aplica animaciones automáticas de entrada y salida a elementos controlados por x-show o x-if. Puedes usar las clases por defecto o personalizar duración y easing:
<div x-show="open" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100">...</div>Si usas Tailwind (el stack de esta web), las clases de transición de Tailwind encajan directamente con los modificadores enter y leave de Alpine, así que el modal o el desplegable se animan sin una línea de CSS propia.
Estado global con Alpine.store
Cuando varios componentes necesitan compartir datos (un carrito, el usuario, el tema), Alpine.store define un estado global reactivo accesible desde cualquier componente con la propiedad mágica $store. Se registra una vez:
document.addEventListener('alpine:init', () => {
Alpine.store('cart', { items: [], add(item) { this.items.push(item) } });
});Y se consume desde cualquier x-data:
<span x-text="$store.cart.items.length"></span>Hablar con Laravel: fetch desde Alpine
Un buscador que consulta una ruta de Laravel
Alpine no sustituye al backend: lo consulta. Un patrón habitual es disparar un fetch a una ruta de Laravel cuando cambia el estado y volcar la respuesta en la vista. Un buscador mínimo:
<div x-data="{ q: '', results: [], async search() {
const res = await fetch('/api/search?q=' + this.q);
this.results = await res.json();
} }">
<input type="search" x-model="q" @input.debounce.500ms="search">
<template x-for="r in results" :key="r.id">
<p x-text="r.title"></p>
</template>
</div>El modificador .debounce.500ms retrasa la petición hasta que el usuario deja de escribir, un detalle que evita golpear tu API en cada pulsación.
Protección CSRF en peticiones POST con fetch
Para peticiones POST, Laravel exige el token CSRF. La forma más limpia es leer la meta tag que genera Blade y enviarla en la cabecera X-CSRF-TOKEN:
fetch('/api/likes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ post_id: 42 })
});Los starter kits estándar ya incluyen esa meta tag en el layout; si el tuyo no, añádela en el head.
Ejemplo completo: galería con filtros y modal de detalle
Juntemos las piezas en un ejemplo real: una galería de proyectos con filtros por categoría y un modal que carga el detalle desde una ruta de Laravel. El estado guarda la categoría activa y el proyecto seleccionado; el filtrado ocurre en el cliente sobre la colección que pinta Blade, y el modal hace fetch a /api/projects/{id} para obtener la descripción completa:
<div x-data="{
category: 'todos',
selected: null,
projects: @json($projects),
filtered() {
return this.category === 'todos'
? this.projects
: this.projects.filter(p => p.category === this.category);
},
async openDetail(id) {
const res = await fetch('/api/projects/' + id);
this.selected = await res.json();
}
}">
<button @click="category = 'todos'">Todos</button>
<button @click="category = '3d'">3D</button>
<button @click="category = 'web'">Web</button>
<div class="grid">
<template x-for="project in filtered()" :key="project.id">
<button @click="openDetail(project.id)" x-text="project.title"></button>
</template>
</div>
<div x-show="selected" x-transition>
<h3 x-text="selected.title"></h3>
<p x-text="selected.description"></p>
<button @click="selected = null">Cerrar</button>
</div>
</div>Con ese único bloque, la galería filtra al instante, el modal se anima con x-transition y el detalle viaja desde Laravel sin recargar la página. El coste total: 15 KB de JavaScript.
Conclusión
Alpine.js convierte las vistas Blade de Laravel 13 en interfaces reactivas sin SPA, sin compilación y con una curva de aprendizaje de una tarde. Úsalo para la capa de interacción pura, deja Livewire y HTMX para la lógica de servidor y tu stack seguirá ligero. Si quieres profundizar, en este blog tienes guías de HTMX 2 y Livewire 4 para comparar enfoques, además del resto de tutoriales de desarrollo web con Laravel 13 para completar el stack.