DOM Manipulation

El DOM (Document Object Model) es el puente entre tu HTML y JavaScript. Es la representación en forma de árbol que el navegador crea de tu página, y que JS puede leer y modificar en tiempo real. Dominar el DOM es lo que te permite crear páginas interactivas, dinámicas y reactivas.

Qué es el DOM

Cuando el navegador carga una página HTML, parsea el código y construye una estructura de árbol donde cada etiqueta, atributo y texto se convierte en un nodo. Este árbol es el DOM. La raíz del árbol es document, y a partir de ahí se ramifica en html, head, body, y todos los elementos que hayas escrito. Cada nodo es un objeto de JavaScript con propiedades y métodos que te permiten leer y modificar la página sin tener que recargarla.

Es importante entender la diferencia entre el código HTML fuente (lo que escribiste en el archivo) y el DOM (lo que el navegador interpreta y renderiza). JavaScript modifica el DOM, no el HTML fuente. Si inspeccionás el código fuente con "View Source" vas a ver el HTML original, pero si usás DevTools vas a ver el DOM actual que puede haber sido modificado por JS.

HTML
<!-- Arbol DOM simplificado de esta estructura -->
<!DOCTYPE html>
<html>
  <head>
    <title>Mi Página</title>
  </head>
  <body>
    <h1>Hola</h1>
    <p class="intro">Párrafo</p>
    <ul id="lista">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  </body>
</html>

El DOM tiene varios tipos de nodos. Los más importantes son los element nodes (etiquetas HTML como <div>, <p>), los text nodes (el texto dentro de las etiquetas), los attribute nodes (los atributos como class, id, href), y el document node (la raíz). En la práctica, vas a trabajar casi exclusivamente con element nodes y sus propiedades. Los text nodes aparecen cuando usás childNodes o firstChild, pero generalmente usás children que filtra solo los elementos.

JavaScript
// Tipos de nodos
document.nodeType;         // 9 (DOCUMENT_NODE)
document.body.nodeType;    // 1 (ELEMENT_NODE)
document.body.firstChild;  // puede ser un text node (espacios en blanco)

// Constantes de tipo de nodo
console.log(Node.ELEMENT_NODE);   // 1
console.log(Node.TEXT_NODE);      // 3
console.log(Node.COMMENT_NODE);   // 8
console.log(Node.DOCUMENT_NODE);  // 9

// Nodos vs elementos (diferencia clave)
const body = document.body;
body.childNodes;   // TODOS los nodos (text, comments, elements)
body.children;     // Solo elementos HTML (HTMLCollection)

// Verificar tipo de nodo
const primerHijo = body.firstChild;
if (primerHijo.nodeType === Node.TEXT_NODE) {
    console.log("Es un nodo de texto");
}

children vs childNodes

Usá siempre children (que devuelve solo elementos HTML) en vez de childNodes (que incluye text nodes de espacios en blanco, comentarios, etc.). children devuelve una HTMLCollection que es iterable con for...of. childNodes devuelve una NodeList viva que puede incluir nodos que no te interesan.

Selección de elementos

Antes de poder modificar un elemento del DOM, primero necesitás seleccionarlo. JavaScript ofrece varios métodos para esto. Los métodos modernos (querySelector y querySelectorAll) son los más usados porque aceptan cualquier selector CSS, lo que los hace extremadamente flexibles. Los métodos clásicos como getElementById siguen siendo útiles y un poco más rápidos cuando solo necesitás buscar por ID.

Escribí un selector CSS en el campo de arriba y observá cómo los elementos que matchean se resaltan en el mini DOM. Usá los quick selectors para probar los más comunes, o hacé clic en cualquier elemento del mini DOM para generar su selector automáticamente. Después, ejecutá acciones sobre los elementos seleccionados: cambiar texto, agregar clases, modificar estilos, clonar o eliminar. El panel derecho muestra el código JavaScript equivalente.

Playground — DOM Selection & Manipulation
JavaScript
// === Metodos MODERNOS (recomendados) ===

// querySelector — devuelve el PRIMER elemento que matchea
const titulo = document.querySelector("h1");
const primerItem = document.querySelector(".lista li");
const boton = document.querySelector("#mi-boton");
const compuesto = document.querySelector("nav a.active");

// querySelectorAll — devuelve TODOS los que matchean (NodeList estatico)
const todosLi = document.querySelectorAll("ul li");
const todosParrafos = document.querySelectorAll("p.intro, p.destacado");
const inputs = document.querySelectorAll("input[type='text']");

// === Metodos CLASICOS (aun validos) ===

// getElementById — un solo elemento por ID (el mas rapido)
const header = document.getElementById("site-header");

// getElementsByClassName — colecciones vivas por clase
const cards = document.getElementsByClassName("card");
// HTMLCollection (viva): se actualiza sola si el DOM cambia

// getElementsByTagName — colecciones vivas por etiqueta
const imagenes = document.getElementsByTagName("img");

// getElementsByName — por atributo name (para forms)
const radios = document.getElementsByName("genero");

// === NodeList estatica vs HTMLCollection viva ===

// querySelectorAll: NodeList ESTATICA (no se actualiza)
const items = document.querySelectorAll(".item");
// Si agregas un .item nuevo al DOM, 'items' NO cambia

// getElementsByClassName: HTMLCollection VIVA (si se actualiza)
const vivos = document.getElementsByClassName("item");
// Si agregas un .item nuevo al DOM, 'vivos' SI lo incluye

Una diferencia importante es que querySelectorAll devuelve una NodeList estática (una "foto" del DOM en ese momento), mientras que getElementsByClassName y getElementsByTagName devuelven colecciones vivas que se actualizan automáticamente cuando el DOM cambia. En la práctica, la versión estática es más predecible y menos propensa a bugs. Para iterar sobre los resultados de querySelectorAll, podés usar forEach, for...of, o convertirlo a array con [...items].

JavaScript
// Iterar sobre querySelectorAll
const botones = document.querySelectorAll(".btn");

// forEach (disponible en NodeList modernas)
botones.forEach(btn => {
    console.log(btn.textContent);
});

// for...of
for (const btn of botones) {
    console.log(btn.textContent);
}

// Convertir a array para usar metodos de array
const arrayBotones = [...botones];
const nombres = arrayBotones.map(b => b.textContent);

// Buscar dentro de un elemento (no en todo el document)
const nav = document.querySelector("nav");
const links = nav.querySelectorAll("a");  // solo links dentro de nav

// closest — buscar el ancestro mas cercano que matchea
const link = document.querySelector("a");
const li = link.closest("li");          // el li padre mas cercano
const navContainer = link.closest("nav"); // el nav ancestro

// matches — verificar si un elemento matchea un selector
const el = document.querySelector("div");
el.matches(".card.active");  // true si tiene ambas clases
el.matches("section > div"); // true si es hijo directo de section

Buena práctica

Usá querySelector y querySelectorAll como tus herramientas principales. Solo usá getElementById cuando buscás por ID y necesitás el máximo rendimiento (por ejemplo en un loop que se ejecuta miles de veces). Y siempre intentá buscar desde el contenedor más cercano en vez de desde document para limitar el área de búsqueda.

Creación y eliminación de nodos

Una de las operaciones más comunes con el DOM es crear nuevos elementos e insertarlos en la página. Esto permite construir interfaces dinámicas: listas que se agregan items, modales que aparecen al hacer click, notificaciones que se crean y desaparecen. El método document.createElement() crea un nuevo elemento HTML que luego insertás en el DOM con métodos como append, appendChild, prepend o insertBefore.

JavaScript
// === Crear elementos ===

// createElement — crea un elemento HTML
const div = document.createElement("div");
div.textContent = "Hola mundo";
div.className = "card";
div.id = "mi-card";
div.setAttribute("data-role", "info");

// Crear un elemento completo antes de insertarlo
const li = document.createElement("li");
li.textContent = "Nuevo item";
li.classList.add("list-item", "highlighted");

// === Insertar en el DOM ===

const lista = document.querySelector("ul");

// appendChild — agregar como ultimo hijo (metodo clasico)
lista.appendChild(li);

// append — agregar al final (moderno, acepta multiples argumentos y strings)
lista.append(li, "texto suelto", document.createElement("hr"));

// prepend — agregar como PRIMER hijo
lista.prepend(li);

// insertBefore — insertar antes de un elemento referencia
const segundo = lista.children[1];
lista.insertBefore(li, segundo);

// after / before — insertar despues o antes de un elemento (hermano)
const h1 = document.querySelector("h1");
h1.after(div);     // despues del h1
h1.before(div);    // antes del h1

// === Crear e insertar en una sola linea ===
document.querySelector(".container").insertAdjacentHTML(
    "beforeend",
    '<div class="card">Card nueva</div>'
);

El método insertAdjacentHTML es particularmente útil porque te permite insertar HTML como string en posiciones precisas sin tener que crear elementos uno por uno. Las posiciones posibles son "beforebegin" (antes del elemento), "afterbegin" (al inicio del elemento), "beforeend" (al final del elemento, antes de cerrar) y "afterend" (despues del elemento). Es más flexible que innerHTML += porque no re-parsea todo el contenido existente.

JavaScript
// insertAdjacentHTML — posiciones
const container = document.querySelector(".container");

// beforebegin: antes del container mismo
container.insertAdjacentHTML("beforebegin", "<div>Antes</div>");

// afterbegin: primer hijo del container
container.insertAdjacentHTML("afterbegin", "<p>Primero</p>");

// beforeend: ultimo hijo del container
container.insertAdjacentHTML("beforeend", "<p>Ultimo</p>");

// afterend: despues del container mismo
container.insertAdjacentHTML("afterend", "<div>Despues</div>");

// === Eliminar elementos ===

// remove() — elimina el elemento del DOM (moderno, limpio)
const viejo = document.querySelector(".old-card");
viejo?.remove();

// removeChild() — elimina un hijo desde su padre (clasico)
const lista = document.querySelector("ul");
const primerHijo = lista.firstElementChild;
if (primerHijo) {
    lista.removeChild(primerHijo);
}

// Eliminar todos los hijos de un elemento
const contenedor = document.querySelector(".items");
contenedor.innerHTML = "";  // rapido pero destructivo
// Alternativa mas segura:
while (contenedor.firstChild) {
    contenedor.removeChild(contenedor.firstChild);
}

// Clonar un elemento
const original = document.querySelector(".card");
const clon = original.cloneNode(true);  // true = copia profunda con hijos
const clonSuperficial = original.cloneNode(false); // solo el elemento, sin hijos
document.querySelector(".container").append(clon);

cloneNode y eventos

Al clonar un elemento con cloneNode(true), se copian los atributos HTML (incluyendo id y class) pero no los event listeners agregados con addEventListener. Si clonás un elemento que tiene listeners, vas a tener que re-asignarlos manualmente al clon, o usar event delegation en su lugar.

Modificación de contenido y estilos

Una vez que tenés seleccionado un elemento, podés modificar su contenido de texto, su HTML interno, sus clases CSS, sus atributos y sus estilos inline. Cada forma de modificar tiene sus ventajas y desventajas. Elegir la correcta es importante tanto para la funcionalidad como para la seguridad de tu aplicación.

Para texto, usá textContent (recomendado) o innerText. La diferencia es que textContent devuelve todo el texto incluyendo el de elementos ocultos con CSS, mientras que innerText solo devuelve el texto visible (teniendo en cuenta estilos como display: none). En la práctica, textContent es más rápido y más predecible. Para HTML, usá innerHTML que parsea strings como HTML, pero con un riesgo de seguridad importante.

JavaScript
const titulo = document.querySelector("h1");
const parrafo = document.querySelector("p");

// === Texto ===

// textContent — leer y escribir texto (recomendado)
console.log(titulo.textContent);  // "Hola Mundo"
titulo.textContent = "Nuevo titulo";  // reemplaza todo el texto
parrafo.textContent += " mas texto";  // concatenar (no recomendado, ver abajo)

// innerText — solo texto visible (mas lento)
titulo.innerText = "Solo visible";

// === HTML interno ===

// innerHTML — leer y escribir HTML (PELIGROSO con datos de usuarios)
const container = document.querySelector(".container");
console.log(container.innerHTML);  // todo el HTML interno como string
container.innerHTML = "<h2>Nuevo</h2><p>Contenido</p>";

// === ATENCION: Riesgo XSS con innerHTML ===
// NUNCA hagas esto con datos de usuarios:
const userInput = "<img src='x' onerror='alert(1)'>";
container.innerHTML = userInput;  // EJECUTA el codigo malicioso!

// En su lugar, usa textContent o createElement
const span = document.createElement("span");
span.textContent = userInput;  // seguro, lo trata como texto
container.appendChild(span);

XSS con innerHTML

innerHTML parsea HTML, lo que significa que si insertás contenido proveniente de un usuario o de una API no confiable, podés abrir una vulnerabilidad de Cross-Site Scripting (XSS). Un atacante podría inyectar <script> tags o event handlers maliciosos. La regla es simple: si el contenido viene de un usuario, usá textContent o createElement. Solo usá innerHTML con contenido que vos controlás completamente.

Para modificar clases CSS, la API classList es la herramienta correcta. Es mucho más limpia que manipular className directamente (que trabaja con strings y requiere reemplazar toda la lista de clases). classList tiene métodos para agregar, eliminar, togglear y verificar clases individuales sin afectar las demás.

JavaScript
const card = document.querySelector(".card");

// === classList ===

// add — agregar una o mas clases
card.classList.add("active", "highlighted");

// remove — eliminar una o mas clases
card.classList.remove("hidden", "old-class");

// toggle — agrega si no tiene, quita si tiene
card.classList.toggle("active");     // switch on/off
card.classList.toggle("visible", true);  // forzar a true
card.classList.toggle("hidden", false);   // forzar a false

// contains — verificar si tiene una clase
if (card.classList.contains("active")) {
    console.log("La tarjeta esta activa");
}

// replace — reemplazar una clase por otra
card.classList.replace("old-class", "new-class");

// === Atributos ===

// getAttribute / setAttribute
card.getAttribute("class");           // "card active"
card.setAttribute("id", "card-1");
card.setAttribute("data-index", "0");

// Propiedades directas (mas comunes)
card.id = "card-1";
card.href = "https://ejemplo.com";  // para <a>
card.src = "imagen.jpg";            // para <img>
card.value = "texto";                // para <input>
card.disabled = true;                // para form elements
card.hidden = false;                 // equivalente a removeAttribute("hidden")

// removeAttribute
card.removeAttribute("data-old");

// hasAttribute
if (card.hasAttribute("data-role")) {
    console.log(card.getAttribute("data-role"));
}

// === Estilos inline (usar con moderacion) ===

// style — objeto con todas las propiedades CSS en camelCase
card.style.backgroundColor = "#1a1a2e";
card.style.padding = "1rem";
card.style.borderRadius = "8px";
card.style.display = "flex";

// Leer un estilo computado (el que realmente se aplica)
const styles = getComputedStyle(card);
console.log(styles.color);
console.log(styles.padding);
console.log(styles.getPropertyValue("background-color"));

// cssText — setear multiples estilos de una vez
card.style.cssText = "color: white; padding: 1rem; margin: 0 auto;";

Estilos: CSS classes > inline styles

Evitá modificar estilos con element.style directamente. En su lugar, agregá o quitá clases CSS con classList y definí los estilos en tu hoja de estilos. Esto mantiene la separación de responsabilidades (HTML para estructura, CSS para presentación, JS para comportamiento) y hace tu código mucho más mantenible. Solo usá inline styles para valores dinámicos que no podés predefinir en CSS.

Traversing del DOM

El traversing (o recorrido) del DOM consiste en navegar entre los nodos del árbol usando las relaciones padre-hijo-hermano. Aunque hoy en día se usa menos que antes (porque querySelector es más directo para la mayoría de los casos), knowing cómo moverse por el árbol sigue siendo útil para escribir componentes reutilizables que funcionen independientemente de la estructura del HTML que los rodea, y para event delegation.

JavaScript
const li = document.querySelector("li");

// === Relaciones padre-hijo ===

// Padre
li.parentElement;           // el <ul> o <ol>
li.closest("ul");          // el ancestro ul mas cercano
li.closest(".container");  // el ancestro .container mas cercano

// Hijos (solo elementos, sin text nodes)
li.children;               // HTMLCollection de hijos directos
li.firstElementChild;      // primer hijo que es elemento
li.lastElementChild;       // ultimo hijo que es elemento
li.childElementCount;      // cantidad de hijos elementos

// === Relaciones entre hermanos ===

li.nextElementSibling;     // siguiente hermano elemento
li.previousElementSibling; // hermano anterior elemento

// === Nodos (incluye text nodes y comentarios) ===

li.childNodes;             // NodeList de TODOS los nodos hijos
li.firstChild;             // primer nodo hijo (puede ser text)
li.lastChild;              // ultimo nodo hijo
li.nextSibling;            // siguiente nodo (puede ser text)
li.previousSibling;        // nodo anterior (puede ser text)

// === Ejemplo practico: navegar desde un boton hasta su card ===
const botonEliminar = document.querySelector(".btn-delete");
const card = botonEliminar.closest(".card");
const titulo = card?.querySelector("h3");
const contenedor = card?.parentElement;

// Remover la card cuando se hace click en el boton
botonEliminar?.addEventListener("click", () => {
    card?.remove();
});

El método closest() merece una mención especial porque es probablemente el método de traversing más útil en el JavaScript moderno. Busca hacia arriba en el árbol (desde el elemento actual hasta la raíz) y devuelve el primer ancestro que matchea el selector CSS dado. Esto es ideal para event delegation: en vez de poner listeners en cada botón individual, ponés un solo listener en el contenedor y usás event.target.closest() para encontrar el elemento relevante.

JavaScript
// Event delegation con closest
// Un solo listener maneja clicks en multiples items
const lista = document.querySelector(".todo-list");

lista.addEventListener("click", (e) => {
    // Buscar el boton mas cercano al click
    const btn = e.target.closest(".btn-delete");
    if (!btn) return;  // si no se clickeo un boton, ignorar

    const item = btn.closest(".todo-item");
    item?.remove();
});

// closest tambien funciona sobre si mismo
const navLink = document.querySelector("nav a");
navLink.closest("a");       // se devuelve a si mismo
navLink.closest("nav");     // el nav padre
navLink.closest("div");     // el div ancestro
navLink.closest("section"); // null si no esta dentro de un section

Data attributes (dataset)

Los data attributes son atributos HTML personalizados que empiezan con data- y te permiten almacenar información extra en elementos HTML de forma estándar y semántica. Son la forma correcta de pasar datos desde el HTML al JavaScript (por ejemplo, IDs de bases de datos, configuraciones, estados iniciales). JavaScript los expone a través de la propiedad dataset del elemento, que convierte automáticamente los nombres de kebab-case a camelCase.

HTML
<!-- Data attributes en HTML -->
<div
    class="user-card"
    data-user-id="42"
    data-role="admin"
    data-last-login="2026-01-15"
    data-preferences-theme="dark"
    data-max-items="10"
>
    <h3>Ana García</h3>
</div>

<button data-action="delete" data-id="42">Eliminar</button>
<button data-action="edit" data-id="42">Editar</button>
<li data-todo-id="1" data-completed="false">Comprar leche</li>
JavaScript
const card = document.querySelector(".user-card");

// === Leer data attributes con dataset ===
// HTML: data-user-id="42"  →  dataset.userId (string "42")
// HTML: data-role="admin"   →  dataset.role (string "admin")
// HTML: data-last-login="2026-01-15" → dataset.lastLogin (string)

console.log(card.dataset.userId);    // "42" (siempre string!)
console.log(card.dataset.role);      // "admin"
console.log(card.dataset.lastLogin); // "2026-01-15"
console.log(card.dataset.maxItems);  // "10"

// Los valores SIEMPRE son strings, hay que convertir
const id = Number(card.dataset.userId);        // 42 (number)
const max = parseInt(card.dataset.maxItems, 10); // 10 (number)
const done = card.dataset.completed === "true";   // true (boolean)

// === Escribir data attributes ===
card.dataset.userId = "99";              // setea data-user-id="99"
card.dataset.newField = "valor";        // crea data-new-field="valor"
delete card.dataset.role;                // elimina data-role del HTML

// === Caso practico: botones con data attributes ===
const botones = document.querySelectorAll("[data-action]");

botones.forEach(btn => {
    btn.addEventListener("click", () => {
        const accion = btn.dataset.action;  // "delete" o "edit"
        const id = btn.dataset.id;          // "42"

        if (accion === "delete") {
            console.log(`Eliminar elemento ${id}`);
        } else if (accion === "edit") {
            console.log(`Editar elemento ${id}`);
        }
    });
});

// Seleccionar por data attribute
const admins = document.querySelectorAll("[data-role='admin']");
const pendientes = document.querySelectorAll("[data-completed='false']");
const todosLosData = document.querySelectorAll("[data-]"); // cualquier elem con data-*

Conversión de nombres

La conversión de data- attribute a dataset property sigue esta regla: data-max-items se convierte en dataset.maxItems (se elimina el prefijo data-, cada guion medio separa palabras, y la primera letra de cada palabra despues del primer guion se convierte a mayúscula). data-user-id becomes dataset.userId, data-xml becomes dataset.xml (solo la primera letra despues del guion se capitaliza).

Los data attributes son ideales para almacenar datos que necesitas en JavaScript pero que no tienen significado semántico en HTML. Por ejemplo, data-user-id="42" no significa nada para el navegador o para la accesibilidad, pero es información crucial para tu lógica de JavaScript. También son útiles para CSS: podés seleccionar elementos con [data-theme="dark"] en tus hojas de estilo y combinarlos con attr() para crear estilos basados en datos.

CSS
/* Data attributes en CSS */

/* Seleccionar por data attribute */
[data-tooltip] {
    position: relative;
    cursor: help;
}

/* Seleccionar por valor exacto */
[data-role="admin"] {
    border-color: var(--accent-red);
}

/* Seleccionar si el atributo existe (sin importar el valor) */
[data-completed="true"] {
    text-decoration: line-through;
    opacity: 0.6;
}

/* Seleccionar que contiene un valor */
[data-tags~="javascript"] {
    border-left: 3px solid var(--accent-blue);
}

/* Usar data attribute con attr() para contenido generado */
[data-icon]::before {
    content: attr(data-icon);
}

/* Data attributes con selectors de parcial */
[data-color^="light"] {
    background: #fafafa;
}

Probá en MiniDevTools

Si querés experimentar con lo que vimos en esta sección, probá el HTML Live Preview o el Keyboard Event Viewer.