Cuando el navegador carga una página, convierte el HTML en un árbol de objetos llamado DOM. JavaScript no toca el HTML: toca ese árbol, y el navegador repinta la página al instante con cada cambio.
También te puede interesar
Una lista de la compra, de principio a fin
Para probarlo, guarda el código en un archivo .html, ábrelo en el navegador y abre la consola con F12 (en Mac, Cmd+Opción+J). Todos los ejemplos de esta página son un único archivo, sin nada más que instalar.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Lista de la compra</title>
<style>
.hecho { text-decoration: line-through; color: #5b6b7c; }
</style>
</head>
<body>
<h1 id="titulo">Lista de la compra</h1>
<ul id="lista">
<li>Pan</li>
<li class="hecho">Leche</li>
</ul>
<p class="nota">2 productos</p>
<script type="module">
// 1. Encontrar elementos
const titulo = document.querySelector("#titulo");
const lista = document.querySelector("#lista");
const productos = document.querySelectorAll("#lista li");
console.log(titulo.textContent, "|", productos.length, "productos");
// 2. Cambiar texto, clases y atributos
titulo.textContent = "Compra del sábado";
productos[0].classList.add("hecho");
lista.setAttribute("aria-label", "Productos pendientes");
// 3. Crear elementos nuevos y añadirlos
for (const nombre of ["Huevos", "Café"]) {
const li = document.createElement("li");
li.textContent = nombre;
li.dataset.cantidad = "1";
lista.append(li);
}
// 4. Quitar uno
lista.querySelector("li.hecho:not(:first-child)").remove();
// 5. Recorrer lo que hay
const items = lista.querySelectorAll("li");
document.querySelector(".nota").textContent = `${items.length} productos`;
for (const li of items) {
console.log(li.textContent, li.className || "-", li.dataset.cantidad ?? "-");
}
</script>
</body>
</html>consolaLista de la compra | 2 productos Pan hecho - Huevos - 1 Café - 1
Al terminar, la página muestra el título «Compra del sábado», una lista con Pan tachado, Huevos y Café (la Leche se ha borrado) y la nota «3 productos». Todo eso lo hizo el script: el HTML original no ha cambiado.
Encontrar elementos
| Método | Devuelve | Si no hay ninguno |
|---|---|---|
document.querySelector("#lista li") | El primero que encaja con el selector CSS | null |
document.querySelectorAll("li") | Todos, en una lista que se recorre con for...of | Una lista vacía |
document.getElementById("lista") | El que tiene ese id (sin almohadilla) | null |
elemento.closest("li") | El antepasado más cercano que encaja, o él mismo | null |
Los selectores son los mismos que en CSS: #id, .clase, ul > li, li:first-child. Y se pueden usar sobre cualquier elemento, no solo sobre document: lista.querySelector("li.hecho") busca solo dentro de la lista.
Cambiar lo que ya existe
| Para | Se usa |
|---|---|
| Cambiar el texto | el.textContent = "..." |
| Añadir, quitar o alternar una clase | el.classList.add("x"), remove, toggle |
| Cualquier atributo | el.setAttribute("aria-label", "...") |
Datos propios (data-*) | el.dataset.cantidad = "1" crea data-cantidad="1" |
| Un estilo suelto | el.style.color = "#1e64af", aunque es mejor cambiar una clase |
Crear y borrar
document.createElement("li") crea un elemento que todavía no está en la página; aparece cuando lo colocas con append (al final), prepend (al principio), before o after. Y el.remove() lo quita.
textContent o innerHTML
innerHTML interpreta el texto como HTML, lo que parece cómodo para meter etiquetas de golpe. El problema llega cuando el texto viene de fuera, por ejemplo de un comentario de un usuario:
<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>textContent e innerHTML</title></head>
<body>
<p id="a"></p>
<p id="b"></p>
<script type="module">
const comentario = '<img src="x" onerror="console.log(\'¡código ajeno ejecutado!\')">';
document.querySelector("#a").textContent = comentario;
document.querySelector("#b").innerHTML = comentario;
console.log("#a tiene", document.querySelector("#a").childNodes.length, "nodo de texto");
console.log("#b tiene un", document.querySelector("#b").firstElementChild.tagName);
</script>
</body>
</html>consola#a tiene 1 nodo de texto #b tiene un IMG ¡código ajeno ejecutado!
Con textContent, la etiqueta se muestra como texto y no pasa nada. Con innerHTML, el navegador crea una imagen de verdad; la imagen no carga, se dispara su onerror y se ejecuta el código que el «comentario» traía dentro. Eso es un ataque XSS: con él, alguien podría leer los datos de la sesión de cualquiera que abra la página.
textContent. Usa innerHTML solo con HTML que hayas escrito tú, nunca con datos de usuarios, de la URL ni de un servidor ajeno.El error frecuente: el script no encuentra nada
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Script en el head</title>
<script>
const titulo = document.querySelector("#titulo");
console.log("titulo vale", titulo);
titulo.textContent = "Nuevo título";
</script>
</head>
<body>
<h1 id="titulo">Título</h1>
</body>
</html>consolatitulo vale null Uncaught TypeError: Cannot set properties of null (setting 'textContent')
El script está en el <head> y se ejecuta en cuanto el navegador llega a él, cuando el <h1> todavía no existe. querySelector devuelve null, y la línea siguiente falla. La solución es que el script espere a que la página esté leída: con type="module", como en los ejemplos de arriba, o con defer. Lo explicamos con detalle en la etiqueta script.
Cada página de esta sección se abrió en Chromium 131, el motor de Chrome, controlado con Playwright, antes de publicarla; lo que aparece como consola es lo que imprimió de verdad. Documentación oficial: Introducción al DOM en MDN.