htmx 4: qué rompe, qué gana y por qué npm te sigue dando la 2
Ves el hilo en Hacker News: htmx 4 released. Vas a tu proyecto, corres npm update htmx.org, arrancas el servidor.
No pasa nada. Sigues en la 2.0.10.
No es tu lockfile. No es la caché de npm. Es una decisión: el equipo publicó htmx 4.0.0 el 28 de agosto de 2026 bajo el dist-tag next, no bajo latest. Y no piensa moverla hasta principios de 2027.
El motivo lo dicen ellos con todas las letras: "we do not want to force-upgrade users who are relying on non-versioned CDN URLs for htmx". Miles de páginas apuntan a una URL de CDN sin versión. Cambiar latest sería reescribir el runtime de todas esas páginas de golpe, sin que nadie hubiera tocado un commit.
Y ahí está lo interesante de esta release, que no va de features.
Cuando el propio equipo decide no empujarte su major, te está diciendo dos cosas. La primera: rompe lo suficiente como para no fiarse. La segunda, la que te sirve hoy: tienes meses para prepararte, no horas.
Dos frases de contexto por si no usas htmx. Es la librería que trae HTML del servidor y lo intercambia dentro del DOM usando atributos en el markup, en lugar de mantener un árbol de componentes en el cliente. Es el modelo opuesto al de Angular o React: tu estado vive en el servidor y el navegador solo pega parches de HTML.
Vamos a lo que cambia.
Cómo instalar htmx 4 hoy (y por qué npm te da la 2.0.10)
A día de hoy el dist-tag latest de npm sigue apuntando a htmx 2.0.10, y next apunta a htmx 4.0.0. Por eso npm install htmx.org te instala la 2 aunque htmx 4 lleve publicada desde el 28 de agosto de 2026.
Todo convive en el mismo paquete:
npm install htmx.org # te instala 2.0.10
npm install htmx.org@next # te instala 4.0.0
npm install htmx.org@4.0.0 # explícito, el que yo usaría
Y antes de tocar nada, la herramienta que el equipo publicó junto a la release:
npx htmx.org@4.0.0 upgrade-check -- ./templates
npx htmx.org@4.0.0 upgrade-check -- ./templates escanea tus plantillas y te marca los atributos eliminados o renombrados. Tarda segundos y te da el tamaño real del problema, que es exactamente lo que necesitas antes de decidir nada.
La 2.x, por cierto, tiene soporte indefinido. No hay reloj corriendo.
Cambio 1: la herencia de atributos ahora se pide por escrito
En htmx 2, un atributo puesto en un padre lo heredaban todos los hijos. Era cómodo hasta que dejaba de serlo, y entonces aparecían hx-disinherit y hx-inherit para apagar y encender esa magia a mano.
En htmx 4 no se hereda nada salvo que lo digas, con el sufijo :inherited:
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/item/1">Delete</button>
</div>
Sin ese sufijo, el botón no pregunta nada. Borra.
Consecuencia directa: hx-disinherit y hx-inherit desaparecen, porque ya no hay nada que desheredar.
Si necesitas la herencia implícita de htmx 2 mientras migras, htmx.config.implicitInheritance = true la devuelve. A diferencia de fetch(), este cambio sí tiene marcha atrás.
Este es el cambio que más plantillas rompe, y rompe de la peor manera posible: no lanza errores. Deja de hacer cosas. Tu confirmación desaparece, tu indicador de carga desaparece, tu hx-target heredado deja de aplicarse y el swap aterriza en otro sitio. Todo con la consola limpia.
Es el mismo patrón que analicé cuando Starlight 0.42 rompió el menú móvil en silencio: los breaking changes que te cuestan dinero no son los que revientan el build, son los que pasan los tests y llegan a producción con la UI medio muerta.
Cambio 2: fetch() nativo y adiós a XMLHttpRequest
htmx 4 reescribe todo el motor de peticiones sobre la API nativa fetch() y elimina XMLHttpRequest. Es el único cambio de la major que no se puede revertir por configuración, y la doc es tajante: "All requests use the native fetch() API. This cannot be reverted."
El motor de peticiones entero está reescrito sobre fetch(). Lo que se lleva por delante:
- Todos los eventos
htmx:xhr:*desaparecen. Cualquier cosa colgada de ellos deja de dispararse. - El timeout por defecto pasa a ser de 60 segundos (60000 ms). En htmx 2 era ilimitado. Si tienes un endpoint de informes que tarda dos minutos, en htmx 4 muere solo.
Ese timeout es de los cambios que más me gustan. Y de los que más incidentes van a provocar la primera semana: un default sano que rompe justo el caso raro que nadie documentó.
Se revierte con una línea: htmx.config.defaultTimeout = 0.
Cambio 3: todos los eventos se renombran
htmx 4 unifica los nombres bajo el patrón htmx:phase:action — fase, acción y, si hace falta, subacción, separadas por dos puntos:
| htmx 2 | htmx 4 |
|---|---|
htmx:beforeRequest |
htmx:before:request |
htmx:afterRequest |
htmx:after:request |
htmx:beforeSwap |
htmx:before:swap |
htmx:afterSwap |
htmx:after:swap |
htmx:configRequest |
htmx:config:request |
htmx:responseError |
htmx:response:error |
Además, varios errores colapsan en uno: htmx:sendError, htmx:swapError, htmx:targetError y htmx:timeout pasan todos a ser htmx:error. Y los eventos de validación (htmx:validation:*) desaparecen.
Si tienes una capa de logging o de telemetría colgada de estos eventos, esta es la parte mecánica de la migración: tediosa, pero visible y buscable.
Otros breaking changes de htmx 4 que no salen en el titular
Además de la herencia, fetch() y los eventos, htmx 4 trae cinco cambios menores que rompen en silencio.
Ahora se swapea casi todo. htmx 4 hace swap de todas las respuestas HTTP menos 204 y 304. En htmx 2, los 4xx y 5xx no swapeaban.
Esto es excelente si devuelves HTML de validación con un 422: se acabó pelearse con la librería para pintar errores de formulario. Y es una bomba si tu 500 devuelve la página de error completa de tu framework, porque ahora esa página entera se te mete dentro del <div> del target. Si necesitas el comportamiento de htmx 2 mientras migras, htmx.config.noSwap = [204, 304, '4xx', '5xx'] lo restaura.
hx-delete ya no incluye los inputs del formulario que lo envuelve. En htmx 2, cualquier petición que no fuera GET arrastraba los valores del formulario asociado, así que hx-delete se comportaba como hx-post. En htmx 4 la regla excluye también a DELETE, y pasa a comportarse como hx-get: no manda nada. Si dependías de eso:
<button hx-delete="/item/1" hx-include="closest form">Delete</button>
El historial ya no cachea en localStorage. Al pulsar atrás, htmx hace una petición de red real y swapea en <body> o en [hx-history-elt]. Si quieres el cacheo de antes, hay una extensión hx-history-cache que usa sessionStorage.
El orden de los swaps out-of-band se invierte. Ahora el contenido principal swapea primero y los OOB después, en orden de documento. Si tenías scripts que asumían el orden contrario, se van a ejecutar contra un DOM distinto.
Los selectores con espacios en hx-trigger necesitan comillas simples: from:'closest form', target:'.a, .b'.
Y dos más: el modificador queue de hx-trigger se elimina en favor de hx-sync="this:queue all", y las extensiones ya no se activan con hx-ext — incluyes el script y listo.
La tabla de atributos eliminados (y el baile peligroso)
| Eliminado | Reemplazo |
|---|---|
hx-disable |
hx-ignore |
hx-disabled-elt |
hx-disable |
hx-vars |
hx-vals con prefijo js: |
hx-params |
evento htmx:config:request |
hx-prompt |
extensión hx-prompt |
hx-ext |
incluir el script directamente |
hx-disinherit |
— (la herencia ya es explícita) |
hx-inherit |
— (la herencia ya es explícita) |
hx-request |
hx-config |
hx-history |
— (ya no hay caché en localStorage) |
Mira las dos primeras filas juntas, porque ahí hay una trampa preciosa.
hx-disable pasa a llamarse hx-ignore. Y hx-disabled-elt pasa a llamarse… hx-disable. El orden en que hagas ese find & replace decide si tu migración funciona o si conviertes todos tus hx-disable viejos en algo que significa otra cosa.
Primero hx-disable → hx-ignore. Después hx-disabled-elt → hx-disable. Al revés chocan: el segundo paso renombraría a hx-ignore los hx-disable que acabas de crear.
Y ancla la búsqueda al nombre exacto del atributo (hx-disable= o la regex \bhx-disable\b). Con un replace de texto plano, el primer paso entra también dentro de hx-disabled-elt y te lo deja como hx-ignored-elt, que no existe. Nadie te avisa.
Es el tipo de detalle que no se ve en una revisión de PR de 400 líneas de plantillas. Lo mismo que pasaba con los 6 breaking changes de pnpm 12 que sí te afectan: las majors no se rompen en el cambio grande que sale en el anuncio, se rompen en la línea 7 de la tabla de migración.
Novedades de htmx 4: hx-status, hx-partial y morphing nativo
htmx 4 añade tres capacidades que en htmx 2 exigían JavaScript o extensiones: hx-status, <hx-partial> y morphing nativo. Porque no todo es pagar el peaje.
hx-status: comportamiento por código de estado. Acepta código exacto (404), comodín de un dígito (50x) y de rango (5xx), con claves swap:, target:, select:, push:, replace: y transition::
<form hx-post="/save"
hx-status:422="swap:innerHTML target:#errors select:#validation-errors"
hx-status:5xx="swap:none push:false">
</form>
Esto resuelve de raíz el problema que planteaba antes: los 422 pintan errores donde tú digas y los 5xx no ensucian nada. Es la respuesta declarativa a lo que en htmx 2 era un htmx:beforeSwap con un if dentro.
<hx-partial>: varios targets desde una sola respuesta. La alternativa a hx-swap-oob, cada bloque con su target y su swap:
<hx-partial hx-target="#messages" hx-swap="beforeend">
<div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
<span>5</span>
</hx-partial>
Morphing nativo. Nuevos estilos de swap innerMorph y outerMorph con el algoritmo idiomorph, dentro del core: "morph swaps using the idiomorph algorithm. Better for preserving state in complex UIs". Se acabó cargar la extensión para que un swap no te reinicie el foco del input.
Y una lista rápida de lo demás:
- Atributos nuevos:
hx-action(conhx-methodopcional),hx-query(peticiónQUERYcon parámetros en el body),hx-config,hx-ignoreyhx-validate. - Swaps nuevos:
textContentydelete, más los aliasbefore/after/prepend/append. - Scroll con sintaxis distinta:
hx-swap="innerHTML show:top showTarget:#other"donde antes poníasshow:#other:top.
El core viene además con un paquete de extensiones oficiales agrupadas por propósito:
- Streaming:
hx-sse,hx-ws,hx-multipart. - UX:
hx-live,hx-pending,hx-prompt,hx-browser-indicator. - Rendimiento:
hx-preload,hx-history-cache,hx-ptag. - Swaps:
hx-download,hx-head,hx-targets,hx-upsert. - Seguridad:
hx-csp. - Compatibilidad:
htmx-2-compatyhx-alpine-compat.
Y un bundle htmax.js que empaqueta htmx con las más usadas.
Esa última categoría, compatibilidad, es la que convierte esta migración en algo realista.
htmx-2-compat: la vía sensata
Existe una extensión oficial, htmx-2-compat, que restaura los defaults y los nombres de eventos de htmx 2. Lo que no te devuelve es el motor: las peticiones siguen saliendo por fetch() y los htmx:xhr:* no vuelven de ninguna manera.
Eso cambia por completo la estrategia. No tienes que elegir entre quedarte en la 2 o reescribir 300 plantillas en un sprint. Puedes:
- Subir a htmx 4.
- Cargar
htmx-2-compaty comprobar qué sigue funcionando igual y qué no. - Ir apagando comportamientos viejos uno a uno, en PRs pequeños, con la app en producción todo el rato.
Es la diferencia entre una migración y un rewrite.
Con una condición que no es negociable: necesitas tests que verifiquen el HTML que llega y dónde aterriza. Sin eso, quitar comportamientos de compatibilidad es dar palos de ciego, porque los fallos de htmx 4 son silenciosos casi siempre. Esta es exactamente la mentalidad que trabajo en mi curso de Testing: los tests no están para demostrar que el código funciona, están para permitirte cambiarlo. El framework da igual; el criterio de qué merece un test, no.
¿Debo migrar a htmx 4 ahora?
| Tu situación | Qué hacer |
|---|---|
| Proyecto htmx 2 en producción, estable | Corre upgrade-check, guarda el informe, no migres aún |
| Proyecto nuevo que empiezas esta semana | Empieza en htmx.org@4.0.0 directamente |
| Usas URL de CDN sin versión | Fíjala a una versión concreta hoy, antes de 2027 |
Tienes hx-disable o hx-disabled-elt en plantillas |
Anota el orden del rename ahora, mientras lo tienes fresco |
| No usas htmx | Quédate con la decisión del dist-tag, que es lo valioso |
Lo que yo haría esta semana
Corre esto y guarda la salida en el repo:
npx htmx.org@4.0.0 upgrade-check -- ./templates
Ya está. No migres hoy. Lo que necesitas ahora es un número: cuántos atributos tuyos están en esa lista. Con ese número decides en enero si es una tarde o un trimestre, y lo decides con datos en vez de con la sensación que te dejó un hilo de Hacker News.
Y quédate con la lección de fondo, que sirve para cualquier dependencia de tu package.json: latest no significa "la última versión". Significa "la versión que el equipo se atreve a darte por defecto". Cuando esas dos cosas se separan durante seis meses, la distancia entre ellas es el mapa de todo lo que rompe.
Preguntas frecuentes sobre htmx 4
¿Puedo instalar htmx 4 hoy?
Sí. Está publicada como 4.0.0 desde el 28 de agosto de 2026, solo que bajo el dist-tag next en lugar de latest. Instálala con npm install htmx.org@next o, mejor, fijando la versión con npm install htmx.org@4.0.0 para que no te cambie bajo los pies cuando publiquen la siguiente preview.
¿Cuándo pasa htmx 4 a ser la versión latest?
El equipo ha dicho que htmx 4 tomará el tag latest en algún momento de principios de 2027. La razón de esperar es no forzar la actualización a quienes cargan htmx desde una URL de CDN sin versión, que se actualizarían de golpe sin haber tocado su código.
¿Cuánto rompe htmx 4 mi proyecto de verdad?
Depende de cuántos atributos eliminados uses, y eso lo puedes medir hoy con npx htmx.org@4.0.0 upgrade-check -- ./templates. Los tres focos de dolor son la herencia de atributos, que ahora exige el sufijo :inherited; los eventos, que se renombran todos al patrón htmx:fase:acción; y el swap de respuestas 4xx y 5xx, que antes no ocurría y ahora sí.
¿htmx 2 deja de tener soporte cuando la 4 sea latest?
No. El equipo mantiene la rama 2.x con soporte indefinido. No hay una fecha de fin de vida anunciada, así que quedarte en htmx 2 es una decisión válida y no una deuda técnica con cuenta atrás.
¿Merece la pena migrar ya?
Si arrancas un proyecto nuevo, sí: empieza directamente en la 4 y te ahorras la migración entera. Si tienes algo en producción, la vía razonable es subir a la 4 con la extensión htmx-2-compat, que restaura los defaults de htmx 2, y desactivar comportamientos viejos poco a poco en vez de reescribir todas las plantillas de una vez.
¿Qué gano si migro, aparte de estar al día?
Tres cosas concretas: hx-status, que te deja definir swap, target y select por código de respuesta de forma declarativa; el elemento <hx-partial>, que apunta a varios elementos desde una sola respuesta sin hx-swap-oob; y el morphing con idiomorph integrado en el core mediante los swaps innerMorph y outerMorph, sin extensión externa.
Si prefieres ver este tipo de análisis en vídeo, con el proyecto delante, lo publico en el canal de YouTube de Dominicode.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
