Saltar al contenido principal

HTMX + Pico CSS: declaración de guerra al frontend actual

· 9 min de lectura
Oscar Adrian Ortiz Bustos
Ingeniero en Gestión y Desarrollo de Software
Contando lecturas...

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.

WelcomeBanner

La idea en tres líneas​

1

Pico CSS

Apariencia y estilos

2

htmx

Interactividad y requests

3

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íaResponsabilidad
Laravelnegocio, DB, auth
Bladerender HTML
htmxservidor ↔ navegador
Alpineestado local
Pico CSSapariencia

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.

Separación de responsabilidades

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."

— Pico CSS

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.

No es para todo

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.

Dónde le veo más sentido​

Para herramientas internas, APIs con UI administrativa o productos chicos:

Laravel
│
├── Blade
├── htmx
├── Alpine.js
└── Pico CSS

o, llevando la idea minimalista todavía más lejos:

Go
│
├── html/template
├── htmx
└── Pico CSS

Un binario más HTML, prácticamente sin frontend toolchain. La arquitectura completa se resume así:

Browser

Pico CSS + htmx

Backend

Laravel / Go / Django, renderiza HTML

Database

Ejemplo de lo que se puede hacer​

Nada como probarlo en vivo. Abajo tienes el mini CRUD de usuarios de la sección anterior, funcionando de verdad: htmx maneja las requests, Alpine el estado del modal y Pico el estilo. Toca el HTML, CSS o JS y el preview se actualiza solo.

Mini CRUD: htmx + Alpine + Pico CSS

Lo importante aquí no es el JS de arriba (eso solo existe porque el iframe del playground no tiene backend). Lo importante es el HTML: hx-post, hx-delete, hx-target y hx-swap describen toda la interacción con el servidor de forma declarativa, y x-data/@click de Alpine maneja el modal sin que se mezcle con la lógica de red. En un backend real, ese hx-post="/users" volvería con el <article> ya renderizado en HTML y el JS de simulación completo desaparecería.

Conclusión​

Nadie está diciendo que React esté mal, o que las SPA no tengan su lugar. Lo que sí me atrevo a decir es que la industria normalizó cargar un frontend completo con toolchain, bundler, gestor de estado y componentes para hacer cosas que un backend con HTML bien servido resuelve en una fracción del tiempo y del código.

htmx + Pico CSS (con Alpine si hace falta estado local) es, para mí, casi una declaración de guerra al frontend moderno sobredimensionado. No porque React sea el enemigo, sino porque muchas veces el problema que tenemos entre manos no necesita ese nivel de artillería. Para CRUDs, herramientas internas y aplicaciones server-driven, esa simplicidad es una ventaja arquitectónica real, no una limitación.

Escrito por un humano