Lección 11 de 1513 min
APIs del documento y la interfaz
DOM, CSSOM, plantillas HTML, los observadores (Mutation, Intersection y Resize) y las APIs de animación y transiciones de vista.
Piensa en una tienda online moderna: la lista de productos se alarga sola cuando llegas al final, los vídeos empiezan a cargar justo antes de verse, el icono del carrito da un saltito al añadir algo y, al abrir un producto, su foto «vuela» hasta su sitio en la ficha. Nada de eso necesita librerías: son APIs que el navegador ya trae.
En la lección sobre el DOM viste el árbol que el navegador construye a partir del HTML. Aquí verás las herramientas para trabajar con él: leerlo y modificarlo, enterarte de sus cambios sin preguntar a cada rato y animarlo.
Leer y modificar la página
DOM
La API para buscar, crear y modificar elementos de la página y reaccionar a eventos.
- Mantiene:
- WHATWG
- Estándar:
- DOM Standard
El DOM (Document Object Model) no es solo el árbol: también es la API de JavaScript para manipularlo. Con querySelector() y querySelectorAll() buscas elementos usando selectores de CSS; con createElement(), append() y remove() añades o quitas nodos; y con textContent, classList o setAttribute() cambias su contenido y sus atributos.
Los eventos conectan la página con el usuario: addEventListener("click", …) ejecuta tu función cuando ocurre algo. La mayoría de los eventos burbujean hacia los elementos padre, así que puedes escuchar en un contenedor y averiguar con evento.target.closest() qué hijo lo provocó. Es la delegación de eventos.
Úsalo cuando
Siempre que necesites cambiar la página desde JavaScript sin un framework, o al escribir un script pequeño, una directiva o un componente de bajo nivel.
Ten en cuenta
No insertes texto del usuario con innerHTML: si contiene algo como <img src=x onerror=…>, ese código se ejecutará (un ataque XSS). Usa textContent. Y si alternas lecturas de tamaños y escrituras de estilos en un bucle, obligas al navegador a recalcular el diseño una y otra vez, como viste en la lección de renderizado.
const lista = document.querySelector("#carrito");
function agregar(producto) {
const item = document.createElement("li");
item.textContent = producto.nombre; // nunca innerHTML con datos del usuario
item.dataset.id = producto.id;
lista.append(item);
}
// Delegación: un solo oyente para todos los elementos, también los futuros
lista.addEventListener("click", (evento) => {
const item = evento.target.closest("li");
if (item) item.classList.toggle("seleccionado");
});CSSOM
El equivalente del DOM para los estilos: leer y cambiar el CSS desde JavaScript.
- Mantiene:
- W3C
Igual que el HTML se convierte en el DOM, el CSS se convierte en el CSSOM (CSS Object Model): un modelo de las hojas de estilo y sus reglas que puedes consultar y modificar. getComputedStyle(elemento) te da el valor final de cada propiedad tras aplicar la cascada, elemento.style cambia los estilos en línea y document.styleSheets da acceso a las hojas cargadas.
Una parte del CSSOM, llamada CSSOM View, aporta lo que más usarás en el día a día: getBoundingClientRect() para saber dónde está y cuánto mide un elemento, matchMedia() para evaluar media queries desde JavaScript y funciones de scroll como scrollIntoView().
Úsalo cuando
Para leer valores calculados, cambiar variables CSS con style.setProperty() o reaccionar desde JavaScript a una media query, como la del modo oscuro.
Ten en cuenta
Antes de tocar estilos uno a uno desde JavaScript, plantéate cambiar una clase o una variable CSS: es más fácil de mantener. Y leer medidas obliga al navegador a calcular el diseño en ese momento, así que agrupa las lecturas.
const raiz = document.documentElement;
// Cambia una variable CSS: todo lo que la use se actualiza
raiz.style.setProperty("--marca", "oklch(60% 0.2 150)");
// Valor final tras la cascada
console.log(getComputedStyle(document.body).backgroundColor); // "rgb(255, 255, 255)"
// Media queries desde JavaScript
const oscuro = matchMedia("(prefers-color-scheme: dark)");
oscuro.addEventListener("change", (e) => {
console.log(e.matches ? "Modo oscuro" : "Modo claro");
});Plantillas HTML
Fragmentos de HTML inertes que clonas desde JavaScript cuando los necesitas.
- Parte de:
- Web Components
El contenido de un elemento <template> se analiza, pero no se muestra ni se ejecuta: sus imágenes no se descargan y sus scripts no corren. Desde JavaScript, plantilla.content te da un fragmento de documento que clonas con cloneNode(true), rellenas y añades a la página tantas veces como quieras.
Es una de las piezas de los Web Components, junto con los custom elements, que crean etiquetas propias con customElements.define("tarjeta-producto", …), y el Shadow DOM, que con attachShadow() encapsula el HTML y el CSS de un componente para que no choquen con el resto de la página.
Úsalo cuando
Para repetir un bloque de HTML (una fila, una tarjeta) desde JavaScript sin construirlo nodo a nodo ni concatenar texto en innerHTML.
Ten en cuenta
Una plantilla nativa no tiene variables ni bucles, como sí tienen las de Angular o Vue: el relleno lo haces tú. Si te ves escribiendo mucho código así, quizá te convenga un framework.
<ul id="productos"></ul>
<template id="plantilla-producto">
<li class="producto">
<img>
<span class="nombre"></span>
</li>
</template>
<script>
const plantilla = document.querySelector("#plantilla-producto");
for (const p of [{ nombre: "Taza", foto: "taza.webp" }]) {
const copia = plantilla.content.cloneNode(true);
copia.querySelector(".nombre").textContent = p.nombre;
Object.assign(copia.querySelector("img"), { src: p.foto, alt: p.nombre });
document.querySelector("#productos").append(copia);
}
</script>Observadores: enterarse sin preguntar
Antes, para saber si un elemento era visible o había cambiado de tamaño, había que escuchar el scroll o el resize de la ventana y medir una y otra vez, lo que hacía la página más lenta. Los observadores invierten el planteamiento: le dices al navegador qué vigilar y él te avisa, agrupando los cambios, cuando ocurre algo.
MutationObserver
Te avisa cuando cambia el DOM: nodos añadidos o quitados, atributos o texto.
- Sustituye a:
- mutation events
Creas un MutationObserver con una función y le dices qué vigilar con observe(nodo, opciones): hijos añadidos o quitados (childList), atributos (attributes), texto (characterData) y si incluir a todos los descendientes (subtree). El navegador agrupa los cambios y te los entrega juntos, como una lista de registros, justo después de que termine el código que los provocó.
Sustituye a los antiguos *mutation events* (DOMNodeInserted y compañía), que se disparaban con cada cambio, ralentizaban la página y los navegadores han ido retirando.
Úsalo cuando
Cuando no controlas el código que modifica el DOM: un widget de terceros, contenido que inserta un CMS o una extensión, o en pruebas que esperan a que aparezca algo.
Ten en cuenta
Si dentro del callback modificas lo mismo que observas, puedes provocar un bucle infinito. Vigila el nodo más concreto posible y llama a disconnect() cuando ya no lo necesites. Y si el cambio lo haces tú, no hace falta observarlo: ya sabes cuándo ocurre.
const contenedor = document.querySelector("#widget-chat");
const observador = new MutationObserver((registros) => {
for (const registro of registros) {
for (const nodo of registro.addedNodes) {
if (nodo instanceof HTMLElement && nodo.matches(".mensaje")) {
avisarConSonido();
}
}
}
});
observador.observe(contenedor, { childList: true, subtree: true });IntersectionObserver
Te avisa cuando un elemento entra o sale de la zona visible.
- Opciones:
- root, rootMargin, threshold
Un IntersectionObserver vigila cuánto de un elemento se cruza con una raíz: la ventana, por defecto, o un contenedor con scroll. Con threshold eliges en qué proporciones quieres aviso (0 = en cuanto asome, 1 = cuando se vea entero) y con rootMargin amplías o reduces esa zona, por ejemplo para empezar a cargar algo 300 píxeles antes de que aparezca.
Cada aviso trae una lista de entradas con isIntersecting, intersectionRatio (la fracción visible, de 0 a 1) y el elemento (target). El navegador hace el cálculo en su propio ciclo de pintado y solo te avisa al cruzar un umbral, así que es mucho más eficiente que medir a mano en cada evento scroll.
Úsalo cuando
Scroll infinito, cargar vídeos o anuncios solo cuando van a verse, contar impresiones reales o lanzar una animación cuando una sección entra en pantalla.
Ten en cuenta
Para imágenes e iframes ya no lo necesitas: el atributo loading="lazy" hace la carga diferida de forma nativa. Y recuerda que el primer aviso llega nada más llamar a observe(), con el estado inicial.
const centinela = document.querySelector("#final-de-lista");
const observador = new IntersectionObserver(
(entradas) => {
if (entradas[0].isIntersecting) cargarMasProductos();
},
{ rootMargin: "300px" }, // avisa 300 px antes de que llegue a verse
);
observador.observe(centinela);ResizeObserver
Te avisa cuando cambia el tamaño de un elemento, no solo el de la ventana.
- Vigila:
- el tamaño de elementos
El evento resize de la ventana no sirve cuando lo que cambia es un elemento: una barra lateral que se pliega, un panel que el usuario redimensiona o una tarjeta que crece con su contenido. ResizeObserver te avisa con el nuevo tamaño de cada elemento observado, en contentRect o, con más detalle, en contentBoxSize y borderBoxSize.
Los avisos llegan después de que el navegador calcule el diseño y antes de que pinte, así que puedes ajustar algo sin que se vea un parpadeo.
Úsalo cuando
Para ajustar un <canvas> o un gráfico al tamaño de su contenedor o decidir cuántos elementos caben. Si solo quieres cambiar estilos según el tamaño del contenedor, usa las consultas de contenedor (@container) de CSS, sin JavaScript.
Ten en cuenta
Si en el callback cambias el tamaño del propio elemento observado, provocas otro aviso; el navegador corta el bucle y muestra en la consola «ResizeObserver loop completed with undelivered notifications».
const contenedor = document.querySelector(".grafico");
const lienzo = contenedor.querySelector("canvas");
const observador = new ResizeObserver(([entrada]) => {
const { width, height } = entrada.contentRect;
lienzo.width = width * devicePixelRatio;
lienzo.height = height * devicePixelRatio;
dibujarGrafico(lienzo);
});
observador.observe(contenedor);Interactivo
IntersectionObserver en directo
Haz scroll dentro de la caja. Cada elemento muestra el último porcentaje visible que le comunicó el observador real del navegador.
↓ Baja para ver los elementos
- PortadaintersectionRatio: 0.00
- NovedadesintersectionRatio: 0.00
- OfertasintersectionRatio: 0.00
- Más vendidosintersectionRatio: 0.00
- OpinionesintersectionRatio: 0.00
- EnvíosintersectionRatio: 0.00
- ContactointersectionRatio: 0.00
Fin de la lista
Con threshold: 0 solo te avisa cuando un elemento empieza a asomar o deja de verse del todo.
Llevas 0 avisos; el primero llega nada más llamar a observe(). Fíjate en que el porcentaje no cambia con cada píxel: es el valor del último aviso.
Animar la interfaz
Web Animations API
Animaciones de CSS controladas desde JavaScript: pausar, invertir, esperar a que acaben.
- Método clave:
- element.animate()
Con elemento.animate(fotogramas, opciones) creas una animación con los mismos fotogramas clave y curvas de aceleración que en CSS, pero desde JavaScript. Devuelve un objeto Animation que puedes pausar (pause()), invertir (reverse()), acelerar (playbackRate) o esperar con await animacion.finished.
Es el mismo motor que mueve las animaciones y transiciones de CSS: document.getAnimations() te devuelve también las que declaraste en la hoja de estilos, y las controlas igual. Si animas transform y opacity, el navegador puede ejecutarlas fuera del hilo principal, y siguen fluidas aunque tu JavaScript esté ocupado.
Úsalo cuando
Cuando la animación depende de datos que solo conoces en JavaScript (de dónde a dónde mover algo), cuando necesitas controlarla mientras se reproduce o hacer algo justo al terminar.
Ten en cuenta
Si la animación es fija y solo depende de una clase o de :hover, CSS es más sencillo. Y respeta a quien pide menos movimiento: comprueba prefers-reduced-motion antes de animar.
const icono = document.querySelector("#icono-carrito");
async function animarCarrito(cantidad) {
if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
const animacion = icono.animate(
[{ transform: "scale(1)" }, { transform: "scale(1.3)" }, { transform: "scale(1)" }],
{ duration: 400, easing: "ease-out" },
);
await animacion.finished;
}
icono.dataset.cantidad = String(cantidad); // se actualiza al terminar
}View Transitions API
Transiciones animadas entre dos estados de la página, o entre dos páginas.
- Método clave:
- startViewTransition()
document.startViewTransition(actualizar) captura el estado actual, ejecuta tu función que cambia el DOM y anima el paso del antes al después; por defecto, con un fundido. Si das el mismo view-transition-name a un elemento en los dos estados (la miniatura de un producto y la foto grande de su ficha), el navegador lo anima de un sitio a otro.
Las animaciones se personalizan con CSS, mediante pseudoelementos como ::view-transition-old() y ::view-transition-new(). Además de estas transiciones dentro de una misma página, existen transiciones entre documentos para webs de varias páginas, que se activan con la regla @view-transition { navigation: auto; } en las dos páginas.
Úsalo cuando
Para dar continuidad visual a un cambio de vista: abrir un detalle, cambiar de pestaña, reordenar una lista. Routers como el de Angular pueden usarla en cada navegación.
Ten en cuenta
Es reciente, y el soporte de las transiciones entre documentos todavía es desigual. Comprueba que existe (document.startViewTransition) y, si no, actualiza sin animación. Mantén las transiciones cortas.
// ficha.js
function abrirFicha(producto) {
// Si el navegador no la soporta, cambia la vista sin animar
if (!document.startViewTransition) return mostrarFicha(producto);
document.startViewTransition(() => mostrarFicha(producto));
}
/* estilos.css: miniatura y foto grande comparten nombre, así que «vuela» */
.miniatura.activa,
.ficha-foto {
view-transition-name: foto-producto;
}Movimiento con moderación
A algunas personas el movimiento en pantalla les provoca mareos. Pueden pedir menos animaciones en los ajustes de su sistema, y tanto CSS (@media (prefers-reduced-motion: reduce)) como JavaScript (matchMedia()) te permiten respetarlo.
Interactivo
Los tres observadores
Activa o desactiva opciones para compararlas cara a cara.
| Característica | MutationObserver | IntersectionObserver | ResizeObserver |
|---|---|---|---|
| Qué vigila | La estructura del DOM | La visibilidad de un elemento | El tamaño de un elemento |
| Te avisa cuando… | Se añaden o quitan nodos, o cambian atributos o texto | Cruza un umbral de visibilidad respecto a la raíz | Cambia su caja de contenido o de borde |
| Opciones clave | childList, attributes, subtree | root, rootMargin, threshold | box |
| Uso típico | Reaccionar a cambios de código ajeno | Scroll infinito, carga diferida, impresiones | Ajustar canvas y gráficos a su contenedor |
| Lo que sustituye | Los mutation events | Medir en cada evento scroll | El resize de la ventana y medir a mano |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Un widget de reseñas de un proveedor externo inserta comentarios en tu página cuando le parece, y quieres resaltar los que mencionan tu marca.
Para recordar
- El DOM también es una API: busca con
querySelector, crea concreateElement, escucha conaddEventListenery usatextContentpara el texto del usuario. - El CSSOM es el DOM de los estilos: valores calculados, variables CSS, medidas y media queries desde JavaScript.
- Un
<template>guarda HTML inerte para clonarlo; junto con los custom elements y el Shadow DOM forma los Web Components. - Los observadores invierten el planteamiento: en lugar de preguntar a cada rato, el navegador te avisa cuando cambia el DOM, la visibilidad o el tamaño.
- Web Animations controla animaciones desde JavaScript; View Transitions anima el cambio entre dos estados de la página.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Por qué no deberías insertar con
innerHTMLel nombre que escribe un usuario?2.¿Qué tiene de especial el contenido de un
<template>?3.Quieres ejecutar algo cuando una sección entra en pantalla. ¿Por qué es mejor IntersectionObserver que escuchar el evento
scroll?4.Dentro del callback de un ResizeObserver cambias el ancho del mismo elemento que observas. ¿Qué puede pasar?
5.¿Qué hace
document.startViewTransition(actualizar)?