Manipulación del DOM con JavaScript
Aprende a manipular el DOM con JavaScript: cambia contenido, atributos y clases, crea, inserta, clona y elimina elementos con ejemplos prácticos.
La manipulación del DOM (Document Object Model) con JavaScript es una habilidad fundamental para los desarrolladores web. El DOM es una representación en árbol, en tiempo real, de la página que el navegador construye a partir de tu HTML; manipularlo significa usar JavaScript para leer y modificar ese árbol en tiempo de ejecución, de modo que la página se actualice sin necesidad de recargar.
Esta guía cubre las cuatro operaciones más habituales:
- Leer y cambiar contenido con
textContenteinnerHTML. - Leer y cambiar atributos con
getAttribute,setAttributeyclassList. - Crear e insertar elementos con
createElement,append,insertBeforeeinsertAdjacentHTML. - Eliminar y reemplazar elementos con
remove()yreplaceWith().
Antes de poder manipular un elemento, primero tienes que encontrarlo. Si aún no te sientes cómodo con getElementById, querySelector y similares, lee Selección de elementos del DOM y Búsqueda: getElement, querySelector primero — todos los ejemplos a continuación asumen que ya tienes una referencia al nodo.
Cambiar el contenido y los atributos de un elemento
Manipular el contenido y los atributos de los elementos del DOM es un aspecto fundamental del desarrollo web dinámico. Al cambiar el contenido, podemos actualizar el texto o el HTML dentro de un elemento. Al modificar los atributos, podemos alterar propiedades como class, id o src. JavaScript ofrece métodos potentes para realizar estas tareas, lo que nos permite crear aplicaciones web responsivas e interactivas. Veamos cómo usar innerHTML, textContent, setAttribute y getAttribute de forma eficaz.
Cambiar el contenido de un elemento
Podemos cambiar el contenido de un elemento utilizando las propiedades innerHTML o textContent.
innerHTML vs textContent
innerHTML nos permite establecer u obtener el contenido HTML de un elemento, incluidas las etiquetas HTML.
<!DOCTYPE html>
<html>
<head>
<title>innerHTML vs textContent</title>
<style>
.content-container {
margin: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.html-content {
color: red;
}
</style>
</head>
<body>
<div class="content-container">
<p id="content">Original paragraph content.</p>
<button id="change-innerHTML">Change using innerHTML</button>
<button id="change-textContent">Change using textContent</button>
</div>
<script>
const content = document.getElementById('content');
const innerHTMLButton = document.getElementById('change-innerHTML');
const textContentButton = document.getElementById('change-textContent');
innerHTMLButton.addEventListener('click', () => {
content.innerHTML = 'New content with <strong class="html-content">HTML</strong> tags.';
});
textContentButton.addEventListener('click', () => {
content.textContent = 'Updated paragraph content without HTML tags.';
});
</script>
</body>
</html>Explicación:
innerHTMLnos permite establecer u obtener el contenido HTML de un elemento, incluidas las etiquetas HTML. En el ejemplo, al hacer clic en el botón "Change using innerHTML" se reemplaza el contenido del párrafo con nuevo contenido que incluye etiquetas HTML, lo que cambia la apariencia del texto.textContentestablece u obtiene el contenido de texto de un elemento sin interpretar las etiquetas HTML. Al hacer clic en el botón "Change using textContent" se reemplaza el contenido del párrafo con texto sin formato, ignorando cualquier etiqueta HTML.
Usa textContent al insertar contenido generado por el usuario para evitar riesgos de seguridad como XSS (Cross-Site Scripting). Asignar una cadena sin procesar de un usuario a innerHTML permite a un atacante inyectar <script> o atributos de controlador de eventos que se ejecuten en tu página. textContent nunca interpreta HTML, por lo que la cadena se muestra literalmente y siempre es segura.
También existe una tercera propiedad, outerHTML, que representa el elemento junto con su propia etiqueta. Al asignarle un valor, el elemento se reemplaza por completo:
// Replace the whole <p> with an <h2>, keeping it in the same position
const p = document.querySelector('p');
p.outerHTML = '<h2>A heading instead</h2>';
// After this, `p` still points at the old, detached node — re-query if you need the new one.Cambiar los atributos de un elemento
Podemos cambiar los atributos de un elemento usando el método setAttribute y recuperarlos con el método getAttribute. Estos métodos son útiles para modificar elementos de forma dinámica en función de las interacciones del usuario u otros eventos.
<!DOCTYPE html>
<html>
<head>
<title>setAttribute and getAttribute</title>
</head>
<body>
<div id="container" class="initial-class">Container content</div>
<button id="change-attribute">Change Attribute</button>
<button id="get-attribute">Get Attribute</button>
<script>
const container = document.getElementById('container');
const changeAttributeButton = document.getElementById('change-attribute');
const getAttributeButton = document.getElementById('get-attribute');
changeAttributeButton.addEventListener('click', () => {
container.setAttribute('class', 'new-class');
alert('Class attribute changed to "new-class"');
});
getAttributeButton.addEventListener('click', () => {
const className = container.getAttribute('class');
alert(`Current class attribute: ${className}`);
});
</script>
</body>
</html>Explicación:
setAttribute('attributeName', 'value'): Este método nos permite establecer un nuevo valor para un atributo específico de un elemento. En el ejemplo, al hacer clic en el botón "Change Attribute" se cambia el atributo class del<div>de "initial-class" a "new-class".getAttribute('attributeName'): Este método recupera el valor actual del atributo especificado. Al hacer clic en el botón "Get Attribute" se muestra una alerta con el valor actual del atributo class del<div>.
Usa siempre setAttribute y getAttribute para atributos personalizados y atributos generados dinámicamente. Para la manipulación de clases, prefiere la API classList que se describe a continuación — edita las clases de forma individual en lugar de sobrescribir toda la cadena class.
Atributos vs. propiedades
Una fuente habitual de confusión: un atributo HTML (lo que está escrito en el marcado) no siempre es lo mismo que la propiedad del DOM (el valor en tiempo real en el objeto JavaScript). getAttribute('value') lee el valor original del marcado, mientras que input.value lee lo que el usuario ha escrito en ese momento. Para los atributos boolean la diferencia es más marcada — checkbox.getAttribute('checked') refleja el marcado, mientras que checkbox.checked refleja el estado actual. Como regla general: usa propiedades (el.id, el.value, el.checked) para valores estándar que cambian con frecuencia, y setAttribute/getAttribute para atributos data- personalizados o atributos de marcado puntuales.
Para leer y escribir atributos data-* existe una API dedicada y ergonómica — la propiedad dataset:
// <div id="card" data-user-id="42" data-role="admin"></div>
const card = document.getElementById('card');
console.log(card.dataset.userId); // "42" (note: data-user-id → userId)
card.dataset.role = 'editor'; // writes data-role="editor"Gestión de clases con classList
classList activa o desactiva clases individuales sin alterar las demás que ya estén presentes:
element.classList.add('new-class'); // add one (or several) classes
element.classList.remove('old-class'); // remove a class
element.classList.toggle('active'); // add if absent, remove if present
element.classList.replace('open', 'shut'); // swap one class for another
console.log(element.classList.contains('active')); // true / falsetoggle acepta un segundo argumento opcional para forzar un estado, lo que resulta útil para sincronizar una clase con una condición: el.classList.toggle('valid', isValid). Para trabajar con estilos en profundidad — incluyendo la lectura y escritura de la propiedad style — consulta Trabajar con estilos en el DOM.
Añadir y eliminar elementos
Podemos añadir nuevos elementos al DOM o eliminar los existentes.
Añadir elementos
Para añadir nuevos elementos al DOM, primero los creamos usando el método createElement y, a continuación, los añadimos a un elemento existente con appendChild o los insertamos en una posición específica con insertBefore.
createElement(), appendChild(), insertBefore()
<!DOCTYPE html>
<html>
<head>
<title>Adding Elements</title>
</head>
<body>
<div id="task-list">
<h2>Task List</h2>
<ul id="tasks">
<li>Initial task</li>
</ul>
<input type="text" id="new-task" placeholder="New task">
<button id="add-task">Add Task</button>
<button id="insert-before">Insert Before First Task</button>
</div>
<script>
const taskList = document.getElementById('tasks');
const newTaskInput = document.getElementById('new-task');
const addTaskButton = document.getElementById('add-task');
const insertBeforeButton = document.getElementById('insert-before');
addTaskButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
taskList.appendChild(newTask);
newTaskInput.value = '';
}
});
insertBeforeButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
const firstTask = taskList.firstElementChild;
taskList.insertBefore(newTask, firstTask);
}
});
</script>
</body>
</html>Explicación:
createElement('tagName'): Este método crea un nuevo elemento especificado portagName. Por ejemplo,document.createElement('li')crea un nuevo elemento<li>.appendChild(newElement): Este método añade un nuevo elemento hijo a un elemento padre especificado. En el ejemplo, al hacer clic en el botón "Add Task" se crea un nuevo elemento de lista (<li>) y se añade a la lista de tareas (<ul>).insertBefore(newElement, referenceElement): Este método inserta un nuevo elemento antes de un elemento de referencia especificado dentro del mismo padre. Al hacer clic en el botón "Insert Before First Task" se crea un nuevo elemento de lista (<li>) y se inserta antes de la primera tarea de la lista.
Para insertar cadenas HTML directamente, considera usar insertAdjacentHTML(). Para reemplazar un elemento existente, element.replaceWith(newElement) es una alternativa moderna a combinar remove() y appendChild().
insertAdjacentHTML y los métodos de inserción modernos
Cuando ya tienes una cadena HTML (en lugar de un nodo construido con createElement), insertAdjacentHTML(position, html) la analiza e inserta en una sola llamada. El argumento position es una de cuatro palabras clave relativas al elemento:
// Given: <div id="box">content</div>
const box = document.getElementById('box');
box.insertAdjacentHTML('beforebegin', '<p>before the box</p>'); // before <div>
box.insertAdjacentHTML('afterbegin', '<p>first child</p>'); // inside, at the start
box.insertAdjacentHTML('beforeend', '<p>last child</p>'); // inside, at the end
box.insertAdjacentHTML('afterend', '<p>after the box</p>'); // after </div>Los navegadores modernos también ofrecen append(), prepend(), before() y after(), que aceptan tanto nodos como cadenas de texto sin formato y pueden recibir varios argumentos a la vez — suelen ser más claros que appendChild/insertBefore:
const list = document.getElementById('tasks');
const item = document.createElement('li');
item.textContent = 'New task';
list.append(item, 'some trailing text'); // append node + string together
list.prepend('Top of the list'); // insert at the startClonar elementos
Para duplicar un nodo existente en lugar de construirlo desde cero, usa cloneNode(deep). Pasa true para copiar el elemento junto con todos sus descendientes; pasa false (o nada) para copiar solo el elemento en sí:
const template = document.getElementById('card');
const copy = template.cloneNode(true); // deep clone, including children
copy.id = 'card-2'; // ids must stay unique
document.body.append(copy);Para marcado complejo y repetitivo, el elemento <template> es la herramienta diseñada específicamente para ello — su contenido permanece inerte hasta que lo clonas.
Eliminar elementos
Para eliminar un elemento, podemos usar el método moderno element.remove().
<!DOCTYPE html>
<html>
<head>
<title>Removing Elements</title>
</head>
<body>
<div id="container">
<p id="paragraph">This is a paragraph.</p>
<button id="remove-paragraph">Remove Paragraph</button>
</div>
<script>
const container = document.getElementById('container');
const paragraph = document.getElementById('paragraph');
const removeParagraphButton = document.getElementById('remove-paragraph');
removeParagraphButton.addEventListener('click', () => {
paragraph.remove();
});
</script>
</body>
</html>Explicación:
element.remove(): Este método moderno elimina directamente del DOM el elemento especificado. En el ejemplo, al hacer clic en el botón "Remove Paragraph" se elimina el elemento<p>de la página.
Usar element.remove() es el enfoque recomendado para cambios de contenido dinámico, como eliminar elementos de un carrito de compras, ya que ofrece una sintaxis más limpia que el método heredado parent.removeChild(child) (que sigue siendo útil cuando necesitas una referencia al nodo eliminado).
Insertar muchos elementos de forma eficiente
Cada vez que insertas un nodo en el documento activo, el navegador puede tener que recalcular el diseño y volver a pintar la página. Hacerlo dentro de un bucle — una vez por elemento — es ineficiente. Un DocumentFragment te permite ensamblar nodos fuera de la pantalla e insertarlos todos en una sola operación:
const list = document.getElementById('tasks');
const fragment = document.createDocumentFragment();
for (let i = 1; i <= 1000; i++) {
const li = document.createElement('li');
li.textContent = `Task ${i}`;
fragment.appendChild(li); // no layout work — fragment is not in the document
}
list.appendChild(fragment); // one insertion, one reflowDos reglas prácticas relacionadas mantienen rápido el código intensivo en DOM: agrupa las lecturas y escrituras (no intercales la lectura de offsetHeight con la modificación de estilos, o forzarás reflujos repetidos), y prefiere construir una cadena y asignarla a innerHTML una sola vez en lugar de muchas llamadas pequeñas a appendChild cuando no estés adjuntando listeners de eventos a cada nodo. Para un tratamiento más detallado, consulta Optimización del rendimiento del DOM.
Ejemplo: Lista de tareas dinámica
Creemos una sencilla aplicación de lista de tareas para demostrar los conceptos anteriores.
<!DOCTYPE html>
<html>
<head>
<title>To-Do List</title>
</head>
<body>
<div id="todo-list">
<h2>My To-Do List</h2>
<ul id="tasks">
<li>Learn JavaScript</li>
</ul>
<input type="text" id="new-task" placeholder="New task">
<button id="add-task">Add Task</button>
</div>
<script>
const taskList = document.getElementById('tasks');
const newTaskInput = document.getElementById('new-task');
const addTaskButton = document.getElementById('add-task');
addTaskButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
taskList.appendChild(newTask);
newTaskInput.value = '';
}
});
taskList.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
event.target.remove();
}
});
</script>
</body>
</html>Explicación:
createElementcrea un nuevo elemento de lista para la tarea, yappendChildlo añade a la lista.- Se adjunta un único listener de clic al
<ul>, no a cada<li>. Como los clics se propagan desde el elemento sobre el que se hace clic hasta sus ancestros, el listener inspeccionaevent.targetpara determinar qué tarea fue clicada y la elimina. Este patrón se llama delegación de eventos, y es la razón por la que el manejador sigue funcionando para tareas que no existían cuando se cargó la página.
La delegación de eventos es la razón por la que no añadimos un listener independiente cada vez que se crea una tarea — un único listener en el padre gestiona los hijos actuales y futuros. Para profundizar en la propagación, event.target vs event.currentTarget y la delegación, lee Manejo de eventos en el DOM.
Cuando una interfaz crece con muchas partes independientes de estado que se actualizan con frecuencia, mantener el DOM sincronizado manualmente se vuelve propenso a errores. Frameworks como React, Vue o Svelte te permiten describir cómo debería verse la interfaz para un estado determinado y gestionan las actualizaciones del DOM por ti. Las técnicas escritas a mano en este capítulo siguen siendo la base sobre la que se construyen esos frameworks.
Conclusión
Dominar la manipulación del DOM es esencial para crear aplicaciones web dinámicas e interactivas. Ahora sabes cómo cambiar contenido (textContent, innerHTML, outerHTML), trabajar con atributos y clases (setAttribute, dataset, classList), crear e insertar nodos (createElement, append, insertAdjacentHTML, cloneNode), eliminarlos y reemplazarlos (remove(), replaceWith()), y agrupar inserciones de forma eficiente con un DocumentFragment.
Para continuar construyendo sobre estas bases:
- Selección de elementos del DOM — encuentra los nodos que quieres cambiar.
- Recorrido del DOM — muévete entre padres, hijos y hermanos.
- Trabajar con estilos en el DOM — lee y escribe CSS desde JavaScript.
- Manejo de eventos en el DOM — responde a la interacción del usuario.