HTMX + Pico CSS: declaración de guerra al frontend actual
Introducción
Voy a ser completamente honesto: cada vez que abro un proyecto nuevo de React para hacer un simple panel interno, algo dentro de mí se apaga un poco. pnpm install, veinte minutos esperando, tres gestores de estado que no necesito, un build pipeline que hay que mantener, y todo para mostrar una tabla con botones de "eliminar". Si estás igual de jodido que yo con esto, quiero contarte de una combinación que me tiene bastante convencido: htmx + Pico CSS (y, si hace falta, un toque de Alpine.js).
No es una tecnología nueva ni mágica. Es, más bien, una negación deliberada de cómo se construye frontend hoy. Y para cierto tipo de proyecto —CRUDs, admin panels, herramientas internas, prototipos— me parece que es objetivamente mejor que meter un SPA completo.

La idea en tres líneas
Pico CSS
Apariencia y estilos
htmx
Interactividad y requests
Backend
Lógica + renderizado HTML
Nada de JSON viajando de un lado a otro para que el cliente lo transforme en DOM. El backend genera HTML directamente y htmx lo inyecta donde le digas. Con Laravel, por ejemplo, la arquitectura se ve así de simple:
Laravel + Blade + htmx + Pico CSS
Un formulario típico:
<form
hx-post="/users"
hx-target="#users"
hx-swap="beforeend"
>
<label>
Nombre
<input name="name" required>
</label>
<button type="submit">Crear usuario</button>
</form>
<section id="users"></section>
Y para estilos, con importar una sola línea:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
>
ya tienes razonablemente estilizados botones, inputs, formularios, tablas, tipografía, diálogos, navegación y layouts básicos. Sin escribir una sola clase tipo class="bg-blue-500 px-4 py-2 rounded-md ...".
El backend devuelve HTML, no JSON
<article id="user-42">
<header>Adrián</header>
<p>Ingeniero de software</p>
<footer>
<button
hx-delete="/users/42"
hx-target="#user-42"
hx-swap="delete"
>
Eliminar
</button>
</footer>
</article>
Pico le da apariencia al <article>, <header>, <footer> y <button> sin que escribas CSS. htmx se encarga de eliminar el elemento del DOM cuando se hace clic. Cero JavaScript propio.
HTML semántico de verdad
Esto es lo que más me gustó cuando empecé a jugar con esto. En vez de construir todo a punta de <div class="card">, <div class="card-header">, vuelves a usar las etiquetas que HTML ya te da:
<article>
<header>...</header>
<main>...</main>
<footer>...</footer>
</article>
Pico está pensado exactamente alrededor de eso. Y conceptualmente, Pico y htmx encajan porque comparten la misma filosofía:
Pico → "usa HTML y deja que CSS haga su trabajo"
htmx → "usa HTML y deja que HTTP haga su trabajo"
Se siente una SPA (sin serlo)
<main class="container">
<nav>
<ul><li><strong>NeanderAdmin</strong></li></ul>
<ul>
<li><a hx-get="/dashboard" hx-target="#content">Dashboard</a></li>
<li><a hx-get="/users" hx-target="#content">Usuarios</a></li>
</ul>
</nav>
<section id="content">...</section>
</main>
Al hacer clic en "Usuarios", htmx dispara GET /users y Laravel responde con el fragmento:
<h1>Usuarios</h1>
<table>
<thead>
<tr><th>Nombre</th><th>Email</th></tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
</tr>
@endforeach
</tbody>
</table>
htmx mete ese HTML dentro de #content, y Pico lo estiliza automáticamente. El resultado se siente como una SPA:
Sidebar/Nav
│
├── Dashboard
├── Usuarios
├── Clientes
├── Facturas
└── Configuración
│
▼
hx-get="/..."
│
▼
#content
pero sin React Router, sin Zustand, sin Axios, sin componentes JSX, sin build pipeline.
Estado local: Alpine.js
htmx resuelve la comunicación con el servidor, pero no maneja estado puramente de UI (abrir un modal, un dropdown, un toggle). Ahí es donde metería Alpine.js, sin que se pise con nada de lo anterior:
| Tecnología | Responsabilidad |
|---|---|
| Laravel | negocio, DB, auth |
| Blade | render HTML |
| htmx | servidor ↔ navegador |
| Alpine | estado local |
| Pico CSS | apariencia |
Un modal con esto queda así:
<div x-data="{ open: false }">
<button @click="open = true">Crear usuario</button>
<dialog :open="open">
<article>
<header>Nuevo usuario</header>
<form
hx-post="/users"
hx-target="#users"
hx-swap="beforeend"
@htmx:after-request="open = false"
>
<input name="name" placeholder="Nombre">
<button>Crear</button>
</form>
</article>
</dialog>
</div>
Cada pieza hace exactamente lo suyo: Pico diseña el diálogo, Alpine lo abre y cierra, htmx manda el POST, Laravel guarda el usuario, Blade devuelve el HTML. Nadie se mete en el trabajo de nadie.
Si notas que Alpine empieza a manejar lógica de negocio o que htmx empieza a necesitar JavaScript custom para todo, es señal de que te estás alejando de la filosofía. La gracia está en que cada capa se quede en su carril.
El punto débil de Pico
Y aquí tengo que ser justo con la herramienta. Pico no es malo, es deliberadamente minimalista. Para CRUD interno, admin panel, dashboard, herramienta personal, prototipo o backoffice, me parece una combinación excelente. Pero si necesitas una UI muy personalizada con identidad de marca fuerte, tarde o temprano vas a terminar escribiendo bastante CSS adicional encima.
Pico no compite con Tailwind, Bootstrap, shadcn/ui o Material UI. Su filosofía es más humilde:
"Haz que HTML normal se vea bien."
Por eso un proyecto con Pico puede arrancar increíblemente chico: un app.js de unos 15 KB, un pico.css, y HTML. Frente a un frontend moderno con cientos de dependencias de pnpm, la diferencia es brutal.
Si el proyecto necesita animaciones complejas, un design system propio elaborado, o interacciones muy ricas del lado del cliente, esta combinación te va a quedar corta. No es la herramienta correcta para todo, es la herramienta correcta para lo que sí resuelve bien.
