¿Qué es el Shadow DOM?

Encapsulación nativa de CSS y Javascript

Sin duda, uno de los conceptos más interesantes de WebComponents (y también de los más complejos) es el Shadow DOM (DOM en la sombra). Para comprenderlo bien, primero debemos tener claro que el DOM es la estructura de elementos que tiene una página, estructurada en forma de árbol, donde se puede ver facilmente los descendientes (hijos) o contenedores (padres) de un elemento.

El navegador, al cargar una página, genera esta estructura en forma de árbol, a la que generalmente llamamos DOM. Cada vez que ocurre un cambio en los elementos de una página, el navegador actualiza esta estructura para reflejarlos.

Aprender más sobre el DOM

¿Qué es el Shadow DOM?

El concepto de Shadow DOM nace de la necesidad de tener una forma de encapsular elementos y mantenerlos aislados del resto de la página, creando algo así como un concepto local, en contraposición al concepto global con el que se solía trabajar en los navegadores.

Quizás, la forma más fácil de entender el concepto de Shadow DOM, es haciendo una analogía precisamente a lo que su propio nombre indica: una sombra de un elemento.

Shadow DOM

Imaginemos que tenemos un árbol DOM del documento de la página en la que nos encontramos. Uno de esos nodos del árbol (destacado en azul) tiene un Shadow DOM (una sombra) en la cuál puede residir otro árbol DOM, que realmente no forma parte del DOM principal de la página.

Como veremos más adelante, entre otras cosas, esta característica nos permitiría algo muy interesante con CSS: aislar los estilos de modo que no afecten a los elementos que están fuera del Shadow DOM, ni tampoco el CSS de fuera afecte a los elementos del Shadow DOM.

Nota: Aunque se suele asociar a WebComponents, realmente el Shadow DOM es una característica independiente que se puede aplicar a un elemento HTML normal, sin necesidad de que sea un componente.

¿Qué es el Virtual DOM?

Aunque tiene cierta relación, el concepto de virtual DOM (o VDOM) no es una alternativa directa de Shadow DOM, pero muchas veces se suelen asociar, ya que debido a sus nombres se suele intuir que son cosas similares o alternativas.

Durante la aparición de los diferentes frameworks del ecosistema Javascript, estos idearon un concepto denominado Virtual DOM: una copia en memoria del DOM de la página, donde se gestionarían directamente los cambios del mismo, para traducirlos posteriormente al DOM real del documento, con el objetivo de acelerar y optimizar los cambios del DOM en la página.

Por ejemplo, la popular librería React introducía el concepto de Virtual DOM para detectar los cambios (diferencias entre árboles), actualizar los nodos afectados (cambios y descendientes a quienes pueden afectar) y posteriormente, actualizarlos en el DOM real, consiguiendo dos cosas principales: velocidad y abstraer de estos cambios para hacerlos de forma automática y menos tediosa:

Virtual DOM

El surgimiento del concepto de Virtual DOM y su definición, ayudó a promover el falso concepto «el DOM es lento», cuando en realidad, lo que puede hacerlo lento es la gestión que hagas (y cómo la hagas) del mismo.

Hay que tener en cuenta también que Shadow DOM es una tecnología existente en los navegadores, mientras que Virtual DOM es un proceso que se implementa y ocurre «fuera» del navegador, en las librerías de Javascript.

¿Cómo crear un Shadow DOM?

Por defecto, los elementos HTML no tienen Shadow DOM. Pero si queremos crear uno y adjuntarlo a un elemento HTML, no tenemos más que utilizar el método .attachShadow() sobre el elemento donde queramos crear el Shadow DOM.

Método Descripción
element.attachShadow(options) Crea y adjunta un Shadow DOM en un elemento.

En breve explicaremos el parámetro obligatorio options y algunas características más, sin embargo para empezar, con este método podríamos crear un Shadow DOM utilizando el siguiente fragmento de código:

const div = document.createElement("div");
const shadow = div.attachShadow({ mode: "open" });

En la primera línea creamos un elemento <div> desde Javascript. También podríamos seleccionar un elemento existente mediante .querySelector(). En este elemento es donde vamos a crear un DOM particular llamado Shadow DOM.

Así pues, en la segunda línea, creamos un Shadow DOM en dicho <div>, estableciéndolo en modo abierto (habitualmente, el más utilizado). En la constante shadow se guarda una al generado, que no es más que el nodo raíz (primer elemento) de ese nuevo DOM particular.

Nota: No puedes adjuntar un en cualquier tipo de elemento HTML. Sólo está permitido en componentes (custom elements), cabeceras (h1, h2, h3, h4, h5 y h6), etiquetas semánticas (article, aside, footer, header, main, nav o section), contenedores (div, span, blockquote, p) o en la propia etiqueta <body>.

Opciones del Shadow DOM

El parámetro options del método .attachShadow() define las características de creación que tendrá el Shadow DOM generado. Veamos que opciones tiene para indicar:

Opción Valores posibles Descripción
mode "open" | "closed" Indica el modo de encapsulación del Shadow DOM. Obligatorio.
delegatesFocus true | false Indica si el Shadow DOM debería obtener el foco.

La opción mode define el modo de encapsulación que queremos que tenga el Shadow DOM.

Modo abierto

En el caso de que indiquemos modo abierto (open), el elemento en el que creamos el Shadow DOM tendrá una propiedad .shadowRoot por la cuál se puede acceder al del elemento, y a partir de ahí, al árbol DOM que contiene:

// Encapsulamiento abierto
const div = document.createElement("div");
const shadow = div.attachShadow({ mode: "open" });

div.shadowRoot === shadow; // true (es el mismo elemento)
Modo cerrado

En el caso de que indiquemos modo cerrado (closed), la propiedad .shadowRoot del elemento nos devolverá , siendo imposible acceder al árbol DOM que contiene. Es recomendable evitar este modo siempre que se pueda, ya que hay muy pocos casos en los que puede ser necesario:

// Encapsulamiento cerrado
const div = document.createElement("div");
const shadow = div.attachShadow({ mode: "closed" });

div.shadowRoot === null; // true (no se guarda la referencia)

En cualquiera de los dos modos anteriores, .attachShadow() siempre devolvera , una referencia al Shadow DOM para acceder a su árbol interno. Esto puede ser particularmente útil en el modo cerrado, ya que no hay otra forma de obtenerlo. Si no guardamos esa referencia, no podremos acceder.

Por último, delegatesFocus es una opción establecida a false por defecto, que indica si queremos que el propio Shadow DOM obtenga el foco o no. Esto puede ser particularmente útil cuando queremos dar estilo a través de la pseudoclase :focus.

Encapsulación con Shadow DOM

Ahora que hemos entendido como funciona y como crear un Shadow DOM quizás no tengamos claro aún para que puede servirnos. La utilidad principal de Shadow DOM es la posibilidad de encapsular elementos, de modo que se encuentren aislados y no repercutan con otros elementos externos.

Encapsular CSS (Estilos locales)

Probablemente, una de las aplicaciones más interesantes del Shadow DOM es la de conseguir estilos CSS locales a un componente. Debido a la naturaleza global tradicional de CSS, siempre hemos creado CSS para aplicar en cascada y que afecte a todos los elementos. Y esto está bien. Es una característica deseable de diseño que bien organizada puede ahorrarnos mucho trabajo.

Sin embargo, en algunos casos, podemos querer aplicar CSS de forma local (como solemos estar más acostumbrados en el mundo de la programación) y que los estilos solo afecten, por ejemplo, al componente donde nos encontramos. Esto permitiría simplificar muchísimo el CSS y haría mucho más sencillo trabajar con estilos en aplicaciones grandes, heredadas o con un gran número de desarrolladores en ella.

Aquí es donde entra Shadow DOM. Por defecto, CSS seguirá teniendo el comportamiento global que ha tenido siempre, permitiendo el uso de la cascada para dar estilos. Sin embargo, gracias a este encapsulamiento podemos aislar un fragmento del DOM y que el CSS dentro del Shadow DOM no afecte al del documento, ni que este CSS global, pueda pasar y aplicarse al del Shadow DOM:

Shadow DOM: Gandalf developer vs Balrog CSS

Esto, llevado a código y aprovechando lo que ya sabemos de WebComponents, se puede realizar de la siguiente forma. Crearemos un componente con un Shadow DOM que en su interior contendrá una etiqueta <style> con estilos CSS locales al componente. Tener en cuenta los estilos dados al elemento <span>. Al estar aislado con un Shadow DOM, solo los <span> del DOM de este componente se verán afectados:

<app-element></app-element>

<script>
class AppElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<style>
span {
background: steelblue;
padding: 5px;
color: white;
}
</style>
<div>
<p>¡Vuelve a la sombra, <span>CSS</span>! ¡NO... PUEDES... PASAR!</p>
</div>
`
;
}
});

customElements.define("app-element", AppElement);
</script>

Observa que la creación del Shadow DOM se suele hacer cuanto antes, en el constructor. Por otro lado, si queremos añadir elementos HTML en el nuevo Shadow DOM creado, tendremos que hacerlo con this.shadowRoot.innerHTML en lugar de this.innerHTML. Hablaremos de este detalle en el siguiente artículo.

Hay que tener claro que al resto de <span> definidos en la página HTML no se le aplicarán los estilos indicados en este componente. De la misma forma, tampoco el DOM de este componente le verá afectado por estilos definidos fuera del mismo, aunque sus selectores encajen.

Nota: En los frameworks de Javascript, una de las formas más extendidas de aislar estilos es utilizar CSS Modules, una estrategia no nativa que cambia las clases de los elementos HTML por hashes autogenerados (usándolos a modo de namespaces) para evitar la cascada CSS y las colisiones en clases con el mismo nombre.

Encapsular Javascript (JS local)

De la misma forma que hemos visto en el apartado anterior, Javascript «sufre» la misma suerte de encapsulamiento que CSS. Imaginemos la siguiente situación, junto al código anterior:

<p>¡Hola a <span>todos</span>!</p>

<app-element></app-element>
<app-element></app-element>
<app-element></app-element>

Ahora, en nuestro Javascript escribimos lo siguiente:

const spans = document.querySelectorAll("span");

Al ejecutar este código Javascript, si tenemos definido un componente con Shadow DOM como vimos en el apartado anterior, la constante spans contendrá un array con un sólo elemento, el <span>todos</span> que tenemos en el <p> de nuestra página principal. Todos los <span>CSS</span> del interior de los componentes no se obtendrán porque están aislados en un DOM particular, en un Shadow DOM.

Por el contrario, si nuestro componente no estuviera aislado con un Shadow DOM sino que sólo utiliza los custom elements sin Shadow DOM como vimos en los primeros temas, obtendríamos un array con 4 elementos: el <span>todos</span> y los 3 <span>CSS</span> que se encuentran en cada componente.

Sabiendo todo esto, podemos utilizar el encapsulamiento de CSS o Javascript a nuestro favor, y crear componentes (o elementos individuales encapsulados) dependiendo de nuestros objetivos, ya sea utilizar CSS/JS global o utilizar CSS/JS local.

Tabla de contenidos