Swaps y morphing
Swaps y morphing
hx-swap responde a cómo entra la respuesta en el target. Por defecto es innerHTML.
<div hx-get="/datos" hx-swap="outerHTML"></div>
Las estrategias
| Valor | Qué hace |
|---|---|
innerHTML | Reemplaza el contenido del target (por defecto) |
outerHTML | Reemplaza el target completo, incluido él mismo |
textContent | Inserta como texto plano, sin parsear HTML |
beforebegin / before | Inserta antes del target, como hermano |
afterbegin / prepend | Inserta como primer hijo |
beforeend / append | Inserta como último hijo |
afterend / after | Inserta después del target, como hermano |
innerMorph | Morphing del contenido del target |
outerMorph | Morphing del target completo |
outerSync | Sincroniza el target con la respuesta preservando identidad |
upsert | Actualiza si existe, inserta si no |
delete | Elimina el target, ignorando la respuesta |
none | No hace swap principal (para OOB o sólo headers) |
Los alias (before, prepend, append, after) son nuevos y más legibles que las cuatro
posiciones de insertAdjacentHTML.
Dónde inserta cada posición
flowchart TD
P["<div id='padre'>"] --> BB["beforebegin — antes del target"]
P --> T["<div id='target'>"]
T --> AB["afterbegin — primer hijo"]
T --> H["contenido existente"]
T --> BE["beforeend — último hijo"]
P --> AE["afterend — después del target"]
Ejemplos típicos:
<!-- Añadir un mensaje al final de un chat -->
<form hx-post="/mensajes" hx-target="#chat" hx-swap="beforeend">…</form>
<!-- Añadir una notificación arriba del todo -->
<button hx-get="/aviso" hx-target="#avisos" hx-swap="afterbegin">Avisar</button>
<!-- Eliminar una fila -->
<button hx-delete="/items/3" hx-target="closest tr" hx-swap="delete">Borrar</button>
<!-- Sólo interesa el header de la respuesta -->
<button hx-post="/log" hx-swap="none">Registrar</button>
textContent: el swap seguro
Nuevo en htmx 4. Inserta la respuesta sin parsearla como HTML:
<span hx-get="/nombre-usuario" hx-swap="textContent"></span>
Si el servidor devuelve <script>alert(1)</script>, aparece como texto literal. Es la opción
correcta cuando el contenido viene de datos que escribió un usuario y no necesitas marcado.
Morphing: innerMorph y outerMorph
Esta es la incorporación estrella de htmx 4. En htmx 2, el morphing vivía en la extensión Idiomorph; ahora está en el core.
Un swap normal destruye y recrea el DOM. Eso significa perder:
- el foco del teclado (el usuario estaba escribiendo en un input)
- la posición del scroll dentro de un contenedor
- el estado de reproducción de un
<video>o<audio> - el estado interno de cualquier widget JS
- la animación CSS en curso
El morphing compara el árbol viejo con el nuevo y aplica sólo las diferencias.
flowchart LR
subgraph N["innerHTML"]
A1[DOM viejo] --> A2[destruir todo] --> A3[crear todo de nuevo]
A3 --> A4["❌ foco, scroll y estado perdidos"]
end
subgraph M["innerMorph"]
B1[DOM viejo] --> B2[diff contra el nuevo] --> B3[tocar sólo lo distinto]
B3 --> B4["✅ foco, scroll y estado intactos"]
end
Cuándo usarlo:
<!-- Un formulario que se revalida en el servidor mientras el usuario escribe -->
<form hx-post="/validar"
hx-trigger="input delay:300ms"
hx-target="this"
hx-swap="outerMorph">
<input name="email" placeholder="Email">
<span class="error"></span>
</form>
Con outerHTML, el usuario perdería el cursor en cada validación. Con outerMorph, el input
sigue enfocado y con el cursor donde estaba.
Controlar el morphing
Tres opciones de configuración global:
| Config | Qué hace |
|---|---|
morphIgnore | Selector de elementos que el morphing no toca en absoluto |
morphSkip | Selector de elementos cuyo nodo no se actualiza |
morphSkipChildren | Selector de elementos cuyos hijos no se recorren |
morphScanLimit | Límite de nodos que el algoritmo escanea buscando coincidencias |
<meta name="htmx-config" content='morphIgnore:"[data-widget-js]"'>
Así proteges los contenedores manejados por bibliotecas externas (un mapa, un gráfico, un editor de texto enriquecido) de que el morphing les reordene los nodos internos.
Para casos puntuales existe hx-preserve, que se ve en el
capítulo 11.
La clave del morphing: los id
El algoritmo empareja nodos por id cuando existen. Sin ids, empareja por posición y tipo de
etiqueta, lo que funciona pero es menos preciso. En listas que se reordenan, pon ids estables:
<ul id="tareas">
<li id="tarea-14">Comprar café</li>
<li id="tarea-9">Pagar la luz</li>
</ul>
Con esos ids, mover un elemento a otra posición no lo destruye: lo mueve.
upsert y outerSync
Dos estrategias menos conocidas pero muy prácticas.
upsert: si el elemento con ese id ya existe en el documento, lo actualiza; si no, lo
inserta. Evita tener que decidir en el servidor si la acción es “crear” o “actualizar”:
<div id="lista" hx-get="/items" hx-swap="upsert"></div>
outerSync: reemplaza el target sincronizando la identidad de los nodos, un punto intermedio
entre outerHTML y outerMorph.
Modificadores
Se escriben en HCON después de la estrategia:
<div hx-get="/datos" hx-swap="innerHTML swap:200ms settle:100ms scroll:top"></div>
| Modificador | Valores | Para qué |
|---|---|---|
swap | tiempo | Retardo antes de insertar (deja correr una animación de salida) |
settle | tiempo | Duración de la fase de settle (animación de entrada) |
transition | true/false | Usa la View Transitions API |
scroll | top/bottom | Salta el scroll del target arriba o abajo |
show | top/bottom | Desplaza para que el target quede visible |
scrollTarget | selector | Aplica scroll a otro elemento |
showTarget | selector | Aplica show a otro elemento |
focusScroll | true/false | Desplaza al elemento que recibe el foco |
ignoreTitle | true/false | No actualiza el <title> con el de la respuesta |
strip | true/false | Elimina el elemento contenedor de la respuesta |
swapEmpty | true/false | Hace swap aunque la respuesta esté vacía |
target | selector | Sobrescribe el target desde el propio hx-swap |
Ejemplos:
<!-- Chat: añadir al final y bajar el scroll -->
<div hx-get="/mensajes" hx-swap="beforeend scroll:bottom"></div>
<!-- Animación de salida de 300 ms antes del reemplazo -->
<div hx-delete="/item/2" hx-swap="outerHTML swap:300ms"></div>
<!-- Que el contenedor con la lista, no el target, haga scroll -->
<div hx-get="/pagina/2" hx-swap="innerHTML scroll:top scrollTarget:#panel"></div>
<!-- No pisar el título de la página -->
<div hx-get="/fragmento" hx-swap="innerHTML ignoreTitle:true"></div>
Las fases del swap y las animaciones
Todo swap pasa por tres fases, y htmx añade clases CSS en cada una:
sequenceDiagram
participant H as htmx
participant D as DOM
H->>D: htmx:before:swap
Note over D: espera `swap:<tiempo>`<br/>(clase htmx-swapping en el target)
H->>D: inserta el HTML nuevo
H->>D: htmx:before:settle
Note over D: clase htmx-added en lo nuevo<br/>durante `settle:<tiempo>`
H->>D: htmx:after:settle
Eso permite animar sin JavaScript:
/* Salida */
.htmx-swapping {
opacity: 0;
transition: opacity 300ms ease-out;
}
/* Entrada */
.htmx-added {
opacity: 0;
}
.fila {
opacity: 1;
transition: opacity 300ms ease-in;
}
<tr class="fila" hx-delete="/items/5" hx-target="closest tr" hx-swap="outerHTML swap:300ms">
El swap:300ms le da tiempo a la transición de opacidad antes de que el nodo desaparezca.
View Transitions
La View Transitions API del navegador anima automáticamente entre dos estados del DOM. htmx la integra:
<!-- Por petición -->
<div hx-get="/detalle" hx-swap="innerHTML transition:true"></div>
<!-- O globalmente -->
<meta name="htmx-config" content="transitions:true">
Para animar un elemento concreto entre estados, dale un view-transition-name en CSS:
.tarjeta-destacada {
view-transition-name: destacada;
}
::view-transition-old(destacada),
::view-transition-new(destacada) {
animation-duration: 300ms;
}
htmx 4 encola las transiciones: si llegan varias respuestas seguidas, se ejecutan en orden en lugar de cancelarse unas a otras, que era el defecto visual más molesto de htmx 2.
Los navegadores sin soporte simplemente hacen el swap sin animar: no hay que hacer nada extra.
Reasignar el swap desde el servidor
return new Response(fragmento, {
headers: {
"Content-Type": "text/html",
"HX-Reswap": "outerHTML transition:true",
},
});
Acepta la misma sintaxis que el atributo, modificadores incluidos.
Elegir la estrategia
flowchart TD
A{¿Qué necesitas?} --> B[Refrescar contenido interno]
A --> C[Reemplazar el elemento entero]
A --> D[Añadir a una lista]
A --> E[Quitar un elemento]
A --> F[Refrescar sin perder foco/scroll]
A --> G[Mostrar texto de usuario]
B --> B1[innerHTML]
C --> C1[outerHTML]
D --> D1["beforeend / afterbegin"]
E --> E1[delete]
F --> F1["innerMorph / outerMorph"]
G --> G1[textContent]
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El elemento se anida dentro de sí mismo | El servidor devuelve el contenedor y usas innerHTML | Usa outerHTML o devuelve sólo el interior |
| Se pierde el cursor al validar mientras se escribe | outerHTML destruye el input | Usa outerMorph |
| La animación de salida no se ve | Falta el modificador swap:<tiempo> | hx-swap="outerHTML swap:300ms" |
El <title> cambia solo | La respuesta trae un <title> | ignoreTitle:true |
| No pasa nada con respuesta vacía | Comportamiento por defecto | swapEmpty:true o devolver 204 a propósito |
| El widget JS se rompe al hacer morph | El morphing reordena sus nodos | morphIgnore o hx-preserve |
Resumen
- El swap por defecto es
innerHTML;outerHTMLreemplaza el elemento completo. - Los alias
before,prepend,append,afterson más legibles que las posiciones adjacent. innerMorphyouterMorphtraen Idiomorph al core: preservan foco, scroll y estado JS.- Los
idestables son la clave para que el morphing empareje bien los nodos. textContentinserta sin parsear: la opción segura para contenido de usuario.- Los modificadores controlan tiempos, scroll, transiciones y título.
- Las clases
htmx-swappingyhtmx-addedpermiten animar con CSS puro. transition:trueactiva las View Transitions, ahora encoladas y sin cancelaciones.
Siguiente: Herencia explícita y HCON →.