El DOM es una interfaz de programación (API) para el código de una página que representa la página como una estructura de objetos en forma de árbol.
Cada elemento HTML (por ejemplo, <p>, <div>, <img>), cada atributo y cada fragmento de texto es un “nodo” separado en este árbol. Usando JavaScript, podemos interactuar con estos nodos para modificar dinámicamente la página: cambiar texto, añadir estilos, crear nuevos elementos o eliminar los existentes. En esencia, el DOM es un modelo “vivo” del documento con el que el código interactúa.
Pero esta apertura tiene una desventaja. Cuando creamos un componente complejo y reutilizable (por ejemplo, un reproductor de video personalizado o un widget de calendario), su estructura interna y sus estilos se vuelven vulnerables. Los estilos CSS de la página principal pueden “filtrarse” accidentalmente en el componente y romper su apariencia. Del mismo modo, el código JavaScript de la página puede modificar sin querer los elementos internos del componente, alterando su lógica.
Para resolver este problema, existe el Shadow DOM.
En esencia, el Shadow DOM es un “DOM dentro de DOM”. Es un árbol de elementos oculto que se adjunta a un elemento de página normal (llamado el “host” o anfitrión) pero que está aislado del DOM principal. Permite al desarrollador crear un límite sellado alrededor de la estructura interna de un componente, protegiéndolo del mundo exterior.
El Shadow DOM te permite adjuntar árboles DOM ocultos a elementos en el árbol DOM regular. Este árbol fantasma comienza con una raíz fantasma (shadow root), bajo la cual se pueden añadir cualesquiera elementos, al igual que en el DOM regular.
Hay varios términos clave relacionados con el Shadow DOM que debes conocer. El Shadow host (o anfitrión) es el nodo del DOM regular al que se adjunta un Shadow DOM. El árbol del DOM dentro del Shadow DOM se conoce como el Shadow tree (o árbol fantasma). El punto donde termina el Shadow DOM y comienza el DOM regular se llama Shadow boundary (o límite fantasma). Finalmente, el nodo raíz del árbol fantasma es la Shadow root (o raíz fantasma).

Puedes manipular los nodos dentro del Shadow DOM al igual que los nodos regulares. La diferencia es que ningún código dentro del Shadow DOM puede afectar a nada fuera de él, garantizando una encapsulación fiable.
Antes de que el Shadow DOM estuviera disponible para los desarrolladores web, los navegadores ya lo usaban para encapsular la estructura interna de los elementos estándar. Por ejemplo, el elemento <video> con controles. Todo lo que ves en el DOM es la etiqueta <video>, pero contiene varios botones y otros controles dentro de su Shadow DOM.
Creando el Shadow DOM
El Shadow DOM se puede crear ya sea de forma imperativa usando JavaScript o de forma declarativa directamente en HTML.
De forma imperativa con JavaScript
Este método es perfecto para aplicaciones renderizadas en el cliente. Seleccionamos el elemento anfitrión y llamamos al método attachShadow() sobre él.
<!-- Marcado HTML -->
<div id="host"></div>
<span>No estoy en el Shadow DOM</span>
// Encuentra el anfitrión y adjunta el Shadow DOM
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
// Crea y añade elementos al árbol del Shadow
const span = document.createElement("span");
span.textContent = "Estoy en el Shadow DOM";
shadow.appendChild(span);
El resultado en la página se verá así:
Estoy en el Shadow DOM
No estoy en el Shadow DOM
De forma declarativa con HTML
Para aplicaciones donde la renderización del lado del servidor es importante, puedes definir el Shadow DOM de forma declarativa usando un elemento <template> con el atributo shadowrootmode.
<div id="host">
<template shadowrootmode="open">
<p>Este párrafo está dentro del Shadow DOM.</p>
<style>
p { color: red; } /* Estos estilos estarán aislados */
</style>
</template>
</div>
Cuando el navegador procesa este código, crea automáticamente una raíz fantasma para el <div> y coloca el contenido del <template> dentro de ella. La etiqueta <template> en sí misma desaparece del árbol DOM principal.
Encapsulación: protección contra JavaScript y CSS
La principal ventaja del Shadow DOM es el aislamiento. Veamos cómo funciona.
Encapsulación de JavaScript
Añade un botón que intente modificar todos los elementos <span> de la página.
// ... Código de creación del Shadow DOM ...
const upper = document.querySelector("#upper-button");
upper.addEventListener("click", () => {
// Este selector busca en todo el documento
const spans = document.querySelectorAll("span");
for (const span of spans) {
span.textContent = span.textContent.toUpperCase();
}
});
Cuando se hace clic en el botón, solo cambia el <span> en el documento principal. El elemento dentro del Shadow DOM permanece intacto porque document.querySelectorAll() no puede “mirar” más allá del límite del Shadow.
Accediendo al Shadow DOM: propiedad shadowRoot y árboles anidados
Cuando llamamos a host.attachShadow({ mode: "open" }), creamos un Shadow DOM “abierto”. Esto significa que podemos acceder a su contenido desde el exterior a través de host.shadowRoot.
// Encuentra spans solo dentro del árbol fantasma de un anfitrión específico
const spansInShadow = host.shadowRoot.querySelectorAll("span");
Si se usa mode: "closed", host.shadowRoot devuelve null, y el acceso externo al árbol fantasma está bloqueado. Este no es un mecanismo de seguridad estricto, sino más bien una convención para los desarrolladores de que los internos del componente no deben ser tocados.
Trabajando con árboles de Shadow anidados
En arquitecturas de componentes complejas, un elemento personalizado puede contener otros elementos personalizados, cada uno con su propio Shadow DOM. Para acceder a un elemento en un árbol de Shadow profundamente anidado, debes atravesar cada shadowRoot secuencialmente.
La estructura de ejemplo es: un componente <nmbrs-form> (el formulario principal), que en su interior tiene un <div> que contiene un <nmbrs-button> (un botón personalizado), y dentro de este <nmbrs-button> hay un elemento <button> HTML real.
Para acceder a este botón desde el contexto global, la ruta es:
// 1. Encuentra el componente raíz en el documento principal
const formComponent = document.querySelector('nmbrs-form');
// 2. Entra en su árbol fantasma
const shadowRoot1 = formComponent.shadowRoot;
// 3. Encuentra el componente de botón anidado
const buttonComponent = shadowRoot1.querySelector('div div.btn-container nmbrs-button');
// 4. Entra en el árbol fantasma de este componente
const shadowRoot2 = buttonComponent.shadowRoot;
// 5. Finalmente, encuentra el elemento del botón
const finalButton = shadowRoot2.querySelector('button#button');
Como una sola cadena:
const button = document.querySelector('nmbrs-form').shadowRoot
.querySelector('div div.btn-container nmbrs-button').shadowRoot
.querySelector('button#button');
Esta larga cadena demuestra el poder de la encapsulación: para llegar a los detalles internos, debes pasar explícitamente a través de cada “límite”. Esto hace que el código sea más predecible y protege los componentes de cambios accidentales.
Encapsulación de CSS
Los estilos definidos en la página principal no afectan a los elementos dentro del Shadow DOM.
/* Este estilo se aplica solo a los spans en el documento principal */
span {
color: blue;
border: 1px solid black;
}
El <span> dentro del árbol fantasma no recibirá estos estilos. Esto resuelve el problema de los conflictos de estilo accidentales.
Estilos dentro del Shadow DOM
Los estilos definidos dentro del árbol fantasma, a su vez, no afectan a la página principal. Hay dos formas principales de añadirlos.
1. Hojas de estilo construibles (Constructable Stylesheets)
Este método permite crear un objeto CSSStyleSheet en JavaScript y aplicarlo a uno o varios árboles fantasma. Es eficiente si tienes estilos compartidos para muchos componentes.
const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black; }");
const shadow = host.attachShadow({ mode: "open" });
// Aplica la hoja de estilos a la raíz fantasma
shadow.adoptedStyleSheets = [sheet];
2. Elemento <style>
Una forma declarativa sencilla es colocar una etiqueta <style> directamente dentro del árbol fantasma (a menudo dentro de un <template>).
<template id="my-element">
<style>
span {
color: red;
border: 2px dotted black;
}
</style>
<span>Estoy en el Shadow DOM</span>
</template>
Shadow DOM y Custom Elements: la combinación perfecta
Todo el poder del Shadow DOM se revela al crear Custom Elements (Elementos Personalizados). Sin encapsulación, serían extremadamente frágiles.
Un elemento personalizado es una clase que extiende HTMLElement. Típicamente, el propio elemento actúa como el anfitrión fantasma, y su estructura interna se crea dentro del árbol fantasma.
Ejemplo de un componente simple <filled-circle>:
class FilledCircle extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
// Crear la implementación interna (p. ej., un círculo SVG)
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("r", "50");
circle.setAttribute("cx", "50");
circle.setAttribute("cy", "50");
// El color proviene del atributo del anfitrión
circle.setAttribute("fill", this.getAttribute("color"));
svg.appendChild(circle);
shadow.appendChild(svg);
}
}
customElements.define("filled-circle", FilledCircle);
Ahora podemos usarlo en HTML como una etiqueta normal sin preocuparnos por su estructura interna:
<filled-circle color="blue"></filled-circle>
<filled-circle color="green"></filled-circle>
Cada uno de estos componentes estará completamente encapsulado y protegido de la página externa.