Manipular el DOM no es cambiar la pantalla: es mantener estado, árbol y eventos sincronizados

Un botón no cambia la pantalla por magia. Cambia una parte de la interfaz porque JavaScript encontró un elemento, escuchó un evento, modificó un estado y dejó que el navegador recalculara lo que debía verse. Cuando una de esas relaciones se rompe, el síntoma suele parecer visual —“la clase no funciona”, “el botón no responde”, “el texto desaparece”—, pero la causa está en la sincronización entre datos, árbol, estilo y eventos.

La documentación de MDN sobre DOM scripting describe el DOM como la representación en árbol que el navegador crea a partir del HTML. JavaScript puede seleccionar nodos, modificar texto y atributos, crear elementos, quitarlos y cambiar clases. La referencia de querySelector() añade una precisión que muchos ejemplos omiten: devuelve el primer elemento que coincide o null si no encuentra ninguno.

La tesis de este artículo es que **manipular el DOM no es cambiar la pantalla con una colección de métodos; es mantener una relación previsible entre el estado que quieres representar y el árbol que el usuario está viendo**. Si seleccionas el nodo equivocado, escuchas el evento equivocado o escribes una clase sin una regla CSS correspondiente, el problema no se arregla añadiendo otra llamada a classList.

El caso: un botón que debe abrir y cerrar un panel

Empieza con HTML que expresa la estructura y una relación accesible:

<button class="toggle" aria-expanded="false" aria-controls="details">
  Mostrar detalles
</button>

<section id="details" hidden>
  <p>Este contenido explica el estado actual del formulario.</p>
</section>

Antes de escribir JavaScript, ya hay una decisión importante: el botón controla el panel mediante aria-controls, y aria-expanded representa si el panel está abierto. El atributo hidden comunica al navegador que el contenido no debe mostrarse. No se trata de adornos que JavaScript debe inventar después; son parte del estado que la interfaz ya puede describir.

Ahora selecciona las referencias:

const boton = document.querySelector(".toggle");
const panel = document.querySelector("#details");

Si el selector está mal escrito o el script se ejecuta antes de que exista el HTML, una de las variables puede ser null. El error que aparezca más tarde —por ejemplo, al llamar addEventListener— será una consecuencia, no la causa. Seleccionar es la primera frontera de la sincronización.

¿Qué ocurre si querySelector() no encuentra el elemento?

Devuelve null. Antes de usar la referencia, revisa el selector, el id o la clase y el momento en que se ejecuta el script. Si el elemento es opcional, decide explícitamente qué debe hacer el programa cuando no exista.

El primer estado debe ser verdadero antes del primer clic

El usuario no empieza cuando pulsa el botón; empieza cuando la página termina de cargar. Por eso debes comprobar que el HTML y el CSS ya representan un estado coherente. El panel está oculto, el botón dice “Mostrar detalles” y aria-expanded vale false. Si el HTML dice una cosa y el script presupone otra, el primer clic puede invertir el problema en lugar de resolverlo.

Define una función que represente el estado completo:

function actualizarPanel(estaAbierto) {
  panel.hidden = !estaAbierto;
  boton.setAttribute("aria-expanded", String(estaAbierto));
  boton.textContent = estaAbierto
    ? "Ocultar detalles"
    : "Mostrar detalles";
}

Esta función no “anima un botón”. Sincroniza tres salidas que describen la misma decisión: visibilidad, estado accesible y texto de la acción. Cuando el estado es true, las tres deben hablar de panel abierto; cuando es false, las tres deben hablar de panel cerrado.

Podrías cambiar solo una clase CSS:

panel.classList.toggle("visible");

Pero si el texto del botón y aria-expanded no cambian, la interfaz tiene tres versiones diferentes de la verdad. A veces la clase funciona y el componente sigue estando mal. Una operación visual no sustituye a un estado completo.

Los eventos explican cuándo debe cambiar la interfaz

Los eventos notifican interacciones y cambios relevantes. La documentación de MDN sobre eventos del DOM explica que los eventos pueden surgir de clics, teclado, formularios, cambios de foco y otras fuentes. También recomienda addEventListener() porque permite registrar varios listeners y retirarlos cuando dejan de ser necesarios.

Conecta el botón a la función:

boton.addEventListener("click", () => {
  const estaAbierto = panel.hidden;
  actualizarPanel(estaAbierto);
});

La expresión panel.hidden contiene el estado actual: si el panel está oculto, el próximo estado debe ser abierto. No necesitas una variable paralela solo para saber si el atributo ya dice true o false. En otros componentes, una variable de estado puede ser necesaria, pero debe existir una sola fuente de verdad o una regla clara que mantenga ambas representaciones alineadas.

El uso de un listener separado también mantiene la conducta fuera del atributo HTML. Los handlers inline como onclick mezclan estructura y lógica, hacen más difícil reemplazar o retirar listeners y pueden ocultar qué código responde a una interacción.

¿Por qué el clic parece no funcionar si el listener está escrito?

Comprueba en orden: que la referencia no sea null, que el script se haya cargado, que el selector coincida con el HTML y que no exista otro elemento cubriendo el botón. Después registra temporalmente el evento y revisa la consola. No agregues listeners adicionales antes de saber si el primero se ejecutó.

classList modifica una decisión de estilo, no inventa el estilo

La propiedad classList ofrece métodos como add(), remove(), toggle() y replace() para manipular las clases de un elemento. Es preferible a montar manualmente una cadena con espacios, pero no crea una regla visual por sí sola.

.panel {
  border: 1px solid #d0d7de;
  padding: 1rem;
}

.panel--highlighted {
  border-color: #2563eb;
  background: #eff6ff;
}
panel.classList.toggle("panel--highlighted", estaAbierto);

La segunda línea solo tiene efecto visible porque la hoja de estilos define qué significa panel--highlighted. Si el nombre está mal escrito, si la regla tiene menor especificidad o si otra regla la sobrescribe, JavaScript puede estar funcionando y la pantalla no reflejarlo.

El segundo argumento booleano de toggle es útil porque convierte la clase en una proyección del estado: se añade cuando estaAbierto es verdadero y se retira cuando es falso. Así evitas que dos clics, un evento repetido o una llamada desde otra parte de la aplicación dejen la clase invertida respecto al dato que intentabas representar.

Texto, atributos y nodos no son la misma operación

El DOM ofrece varias formas de cambiar contenido, y la elección afecta seguridad y estructura. Para texto proporcionado por una persona, textContent trata el valor como texto. Asignar a innerHTML interpreta una cadena como marcado y puede introducir riesgos si la cadena no es confiable.

mensaje.textContent = usuarioEscribio;

Usa innerHTML solo cuando realmente necesitas construir HTML y controlas o sanitizas el contenido. Para un componente principiante, crear elementos con createElement(), asignar textContent y conectarlos con appendChild() hace más visible la diferencia entre texto y estructura.

Por ejemplo, una lista dinámica puede crecer así:

const elemento = document.createElement("li");
elemento.textContent = "Nueva tarea";
lista.appendChild(elemento);

La guía de objetos en JavaScript puede ayudarte a entender por qué estas referencias representan objetos vivos y por qué asignar una variable no copia automáticamente el nodo. Si guardas una referencia y después lo mueves, sigues trabajando con el mismo elemento, no con una fotografía independiente de la pantalla.

Un formulario muestra por qué el evento necesita contexto

Añade un formulario sencillo al componente:

<form id="note-form">
  <label for="note">Nota</label>
  <input id="note" name="note" />
  <button>Añadir</button>
</form>
<ul id="notes"></ul>

Si escuchas el evento click del botón, puedes olvidar que pulsar Enter también debe enviar el formulario. Escuchar el evento submit expresa mejor la intención:

formulario.addEventListener("submit", (evento) => {
  evento.preventDefault();

  const valor = entrada.value.trim();
  if (!valor) return;

  const nota = document.createElement("li");
  nota.textContent = valor;
  notas.appendChild(nota);
  formulario.reset();
});

preventDefault() evita la navegación que normalmente provocaría el envío mientras tú decides qué hacer con el dato. El orden también importa: leer, validar, crear, insertar y limpiar. Si limpias el input antes de guardar el valor, la interfaz responde pero la información desaparece.

Este ejemplo contiene una causa frecuente de errores: confundir el elemento que recibe el evento con el elemento que contiene el dato. El evento es un objeto con información; el formulario tiene controles; el input tiene un valor; la lista recibe un nuevo nodo. El DOM no es una bolsa de métodos aislados: es una relación entre elementos.

Delegación: cuando los elementos aparecen después

Si cada nota tiene un botón para eliminarla, puedes registrar un listener en cada botón al crearlo. También puedes escuchar el clic en la lista y comprobar si el objetivo corresponde a una acción. La segunda estrategia se vuelve útil cuando los elementos son dinámicos, pero exige entender la propagación de eventos y seleccionar el elemento correcto.

notas.addEventListener("click", (evento) => {
  if (!(evento.target instanceof HTMLButtonElement)) return;
  evento.target.closest("li")?.remove();
});

La delegación aprovecha que los eventos pueden burbujear desde el elemento objetivo hacia sus ancestros. No la uses solo porque el ejemplo parece más avanzado. Usa un listener por elemento cuando el conjunto sea pequeño y fijo; considera delegación cuando los hijos se crean o eliminan con frecuencia y el contenedor ofrece un alcance claro.

Si el selector de la acción es demasiado amplio, un clic en otra parte del li puede activar una operación no prevista. Si el listener queda registrado varias veces cada vez que renderizas, una pulsación puede ejecutar la misma acción repetidamente. La arquitectura del evento debe seguir la arquitectura del DOM.

Cuando el contenido cambia, accesibilidad y estado deben viajar juntos

Una interfaz dinámica debe comunicar el cambio más allá de los píxeles. Si un botón abre un panel, aria-expanded debe actualizarse. Si el panel tiene un control interno, el foco debe seguir una ruta razonable. Si un error aparece después del envío, el mensaje debe estar asociado al campo o ser perceptible para quien usa teclado o tecnología asistiva.

La documentación de MDN sobre aria-expanded explica que el atributo indica si un control expande o contrae otro elemento. No reemplaza un HTML semántico bien elegido; completa una relación cuando el componente necesita comunicarla. La accesibilidad no es una capa para agregar al final después de que la manipulación visual ya “funcione”.

También puedes usar elementos nativos como <details> y <summary> cuando el comportamiento que necesitas ya existe en HTML. Si el navegador puede ofrecer el estado y la interacción sin JavaScript, reduces la cantidad de sincronización que debes mantener.

Diagnóstico: cuatro síntomas y su causa más probable

SíntomaPrimera hipótesisComprobación
El botón no responde.Referencia nula, listener no registrado o elemento cubierto.Inspeccionar selector, carga del script y consola.
La clase aparece pero nada cambia.No existe una regla CSS compatible o está sobrescrita.Revisar estilos calculados y nombre exacto.
El texto se muestra como HTML.Se usó innerHTML para dato que debía ser texto.Usar textContent y validar el origen.
Un clic ejecuta dos veces.Listener registrado repetidamente o propagación no controlada.Revisar el ciclo de renderizado y quién registra el evento.

La tabla es útil porque cambia el orden del diagnóstico. En lugar de añadir una llamada, primero comprueba el contrato que se rompió. Un elemento puede existir en el HTML y no ser el que seleccionaste; un evento puede llegar al contenedor y no al botón que imaginabas; una clase puede estar presente y ser visualmente irrelevante.

Una secuencia de trabajo que evita arreglos a ciegas

  1. Describe el estado inicial. ¿Qué ve el usuario antes de interactuar?
  2. Elige HTML nativo cuando exista. Reduce código y mejora el comportamiento básico.
  3. Selecciona con un contrato claro. Decide si esperas uno o varios elementos y qué ocurre si faltan.
  4. Escucha el evento que representa la acción. Para un formulario, suele ser submit, no solo el clic.
  5. Calcula el próximo estado. No inviertas clases sin saber qué estado representan.
  6. Actualiza todas las salidas relacionadas. Texto, atributos, clases, contenido y foco deben coincidir.
  7. Prueba teclado, repetición y ausencia. El componente debe resistir más que el primer clic.

Si sigues esta secuencia, los métodos dejan de ser una lista para convertirse en decisiones. querySelector() resuelve la referencia; addEventListener() conecta un cambio; classList proyecta una condición en estilos; textContent actualiza texto sin interpretarlo; createElement() y remove() cambian la estructura.

La pantalla es el resultado, no el origen de la lógica

Un principiante suele mirar la pantalla y preguntar “¿cómo hago que esto se vuelva azul?”. Una pregunta más útil es “¿qué estado significa que esto debe ser azul, y qué evento puede cambiar ese estado?”. Esa formulación evita que el CSS, el JavaScript y el HTML empiecen a contar historias diferentes.

El DOM te permite modificar una página, pero no decide por ti cuál es el estado correcto. Tú debes definir una representación inicial, una transición y una respuesta cuando el elemento o el dato no existe. Esa es la diferencia entre un efecto que funciona en una demostración y un componente que sigue siendo comprensible cuando recibe entradas reales.

Para seguir conectando estas decisiones, puedes repasar los métodos de arrays en JavaScript, las diferencias entre unidades CSS y la guía sobre accesibilidad web. La manipulación del DOM se vuelve más segura cuando entiendes qué parte pertenece a los datos, qué parte al layout y qué parte a la interacción.

¿Qué debo comprobar primero cuando classList.toggle() no produce cambios visibles?

Comprueba que el elemento sea el correcto, que el nombre de la clase coincida y que exista una regla CSS que use esa clase. Después revisa los estilos calculados para saber si otra regla la está sobrescribiendo.

¿Es mejor escuchar click o submit en un formulario?

Para la acción de enviar un formulario, escucha submit. Así incluyes el clic y la tecla Enter, y puedes llamar a preventDefault() en el punto donde realmente ocurre el envío.

¿Debo usar innerHTML para crear todo el contenido dinámico?

No. Para texto proporcionado por usuarios o fuentes externas, `textContent` es una opción más segura y clara. Usa `innerHTML` solo cuando necesitas marcado y controlas o sanitizas el contenido.

¿Por qué actualizar aria-expanded si el panel ya se ve?

Porque el estado visual no es la única información que debe recibir el usuario. `aria-expanded` comunica la relación de expansión a tecnologías asistivas y debe coincidir con el estado que ve la interfaz.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio