El DOM no es tu HTML: qué muestra realmente el panel
Las siete diferencias entre el documento que enviaste y el árbol que ves en Elements, y por qué esa distinción decide dónde buscas un bug.
Hay una confusión que persiste durante años en gente que ya sabe programar: creer que el panel de Elements muestra el HTML de la página. No lo muestra. Muestra una serialización del árbol del DOM vivo en este preciso instante, que es un objeto distinto, construido a partir del HTML pero modificado por el parser, por los scripts, por los componentes y por el propio motor de renderizado. Cuando el código fuente y el panel discrepan, no hay ningún bug: hay siete mecanismos conocidos, y saber cuál está actuando te dice inmediatamente dónde mirar.
- Distinguir el documento fuente, el DOM y la representación que pinta el panel.
- Enumerar las siete causas de discrepancia entre lo que enviaste y lo que ves.
- Reconocer los distintivos del panel y qué información aporta cada uno.
- Decidir, ante una discrepancia, si el problema está en el servidor, en el parser o en el cliente.
Tres artefactos distintos
El documento fuente es la secuencia de bytes que el servidor envió. Es lo que ves con la opción de ver el código fuente de la página, y lo que devuelve una petición sin ejecutar JavaScript. En una aplicación renderizada en cliente puede ser un body prácticamente vacío.
El DOM es la estructura de objetos que el navegador construyó parseando ese documento y ejecutando después todo el JavaScript que ha corrido hasta ahora. Es un árbol vivo, mutable, con nodos que tienen mucho más estado del que se puede expresar en HTML.
La representación del panel es lo que el frontend de las DevTools dibuja: una serialización del DOM con marcas añadidas —distintivos, nodos plegados, secciones especiales— que existen solo en la interfaz y no corresponden a nada en el documento.
Las discrepancias entre el primero y el segundo son las interesantes. Son siete.
Las siete fuentes de discrepancia
Uno: el parser corrige tu HTML. El algoritmo de parseo de HTML está especificado hasta el último detalle, incluido cómo recuperarse del marcado inválido, y aplica correcciones sin avisar. La más famosa es el tbody: si escribes una table con tr directamente dentro, el parser inserta un tbody que tú no pusiste, y tu selector table > tr deja de casar. También cierra etiquetas que dejaste abiertas, mueve contenido que aparece donde no debe, y descarta elementos en posiciones ilegales.
Dos: los scripts modifican el árbol. Es lo obvio y es la mayoría de los casos. Cualquier framework construye o transforma nodos después de la carga.
Tres: los pseudo-elementos. ::before y ::after no existen en el DOM como nodos accesibles desde JavaScript, pero el panel los muestra como si fueran hijos, porque generan cajas. Si buscas ese nodo con querySelector no lo encontrarás nunca.
Cuatro: el shadow DOM. Los componentes con raíz de sombra tienen un subárbol propio que el panel muestra bajo un nodo especial. La sombra abierta se puede inspeccionar y consultar desde element.shadowRoot; la cerrada aparece en el panel pero no es accesible desde el código de la página. Y los estilos de dentro no salen ni los de fuera entran, salvo por las vías previstas.
Cinco: los elementos del propio navegador. Los controles de formulario, los reproductores de vídeo nativos y otros elementos complejos tienen internamente una estructura de shadow DOM del agente de usuario. Normalmente está oculta, y hay un ajuste para mostrarla. Es la única forma de entender por qué un input de tipo fecha no se deja estilar como querrías.
Seis: la capa superior. Los elementos promocionados por dialog con showModal o por la API de popover se pintan en una capa por encima de todo el documento. El panel los marca con un distintivo y ofrece un enlace a su representación en esa capa, que es donde se explica por qué ningún z-index los tapa.
Siete: el contenido de los iframes. Un iframe no es un nodo con hijos: es un documento separado, con su propio árbol, su propio contexto de ejecución y posiblemente otro origen. El panel deja entrar en él visualmente, lo que hace muy fácil olvidar que las expresiones de la consola no llegan ahí por defecto.
La discrepancia entre el documento fuente y el DOM es exactamente la frontera entre un problema de servidor y un problema de cliente, y es la primera bisección de cualquier bug de renderizado. Si el nodo está en el fuente y no en el árbol, alguien lo quitó y el culpable es JavaScript. Si no está en ninguno de los dos, el servidor no lo mandó y el frontend es inocente. Comprobarlo cuesta un vistazo.
Los distintivos del panel
Junto a ciertos nodos aparecen etiquetas pequeñas. No son decoración: cada una indica una capacidad del elemento y la mayoría son interruptores que activan un overlay o abren una vista.
El distintivo de grid y el de flex indican que el elemento establece ese tipo de contexto de formato, y al pulsarlos se dibuja el overlay correspondiente sobre la página. El de subgrid aparece cuando un contenedor hereda las líneas de su padre.
El de contenedor aparece en elementos con container-type declarado, y es la forma más rápida de comprobar si una container query tiene contenedor donde crees.
El de scroll marca los elementos que son contenedores de desplazamiento, que resuelve de un vistazo la pregunta eterna de por qué el scroll ocurre en un sitio distinto del que esperabas. El de scroll-snap marca los que tienen ajuste de scroll configurado.
El de slot aparece en contenido distribuido dentro de un componente y enlaza con la ranura donde acaba colocándose.
El de capa superior marca los elementos promocionados por diálogo o popover.
Los distintivos se pueden mostrar y ocultar selectivamente desde el menú contextual del propio panel, lo cual es útil cuando el árbol se llena tanto de etiquetas que estorban.
El menú contextual de un nodo ofrece varias opciones de copia y la gente elige la primera sin leer, lo que produce una clase de frustración muy específica. Copiar el elemento te da el HTML externo del nodo tal como está ahora, incluyendo los atributos que los scripts hayan añadido y excluyendo los pseudo-elementos, que no son copiables porque no son nodos. Copiar el HTML interno te da solo el contenido. Copiar el selector genera un selector CSS que identifica ese nodo hoy, generalmente frágil porque se apoya en posiciones nth-child que cambian al siguiente render. Copiar el XPath hace lo mismo con otra sintaxis, y copiar el XPath completo genera la ruta absoluta desde la raíz, todavía más frágil. La consecuencia práctica que importa es esta: los selectores generados por las DevTools son para pegar en la consola ahora mismo, no para escribir un test. Un selector div > div:nth-child(3) > span sobrevive exactamente hasta que alguien añada un elemento, y entonces el test falla en producción por una razón que no tiene nada que ver con lo que el test pretendía comprobar. Para tests, el selector correcto se escribe a mano, y se apoya en lo que semánticamente identifica al elemento: su rol, su nombre accesible o un atributo de datos puesto a propósito. La opción de copiar selector es una comodidad de exploración que se ha colado en miles de suites de tests frágiles.
Cómo se lee la discrepancia en la práctica
Un procedimiento de treinta segundos que ordena todo lo anterior. Ante cualquier “esto no está donde debería”.
Busca el nodo en el árbol. Si está, tu problema es de estilos o de geometría, y el panel de estilos es el destino. Si no está, sigue.
Mira el documento fuente con la opción de ver el código fuente. Si el nodo está ahí, alguien lo quitó después: un breakpoint de eliminación de nodo sobre el padre te dirá quién en un minuto. Si no está, sigue.
Mira la respuesta de la petición del documento en el panel de red, que es lo que llegó de verdad y puede diferir del fuente cacheado. Si no está ahí, es el servidor y ya has terminado en el frontend.
Y si está en el árbol pero document.querySelector devuelve null, tienes uno de los tres casos especiales: es un pseudo-elemento, está dentro de un shadow DOM, o está dentro de un iframe. Los tres se resuelven cambiando dónde preguntas, no qué preguntas.