El Shadow DOM declarativo es una función estándar de la plataforma web que se admite en Chrome desde la versión 90. Ten en cuenta que la especificación de esta función cambió en 2023 (incluido el cambio de nombre de shadowroot
a shadowrootmode
) y que las versiones estandarizadas más actualizadas de todas las partes de la función se lanzaron en la versión 124 de Chrome.
El Shadow DOM es uno de los tres estándares de Web Components, junto con las plantillas HTML y los elementos personalizados. Shadow DOM proporciona una forma de aplicar estilos de CSS a un subárbol de DOM específico y aislar ese subárbol del resto del documento. El elemento <slot>
nos brinda una forma de controlar dónde se deben insertar los elementos secundarios de un elemento personalizado dentro de su árbol de sombras. Estas funciones combinadas permiten un sistema para compilar componentes reutilizables y autónomos que se integran sin problemas en aplicaciones existentes, como un elemento HTML integrado.
Hasta ahora, la única forma de usar Shadow DOM era construir una raíz de sombra con JavaScript:
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
Una API imperativa como esta funciona bien para la renderización del cliente: los mismos módulos de JavaScript que definen nuestros elementos personalizados también crean sus raíces de sombra y establecen su contenido. Sin embargo, muchas aplicaciones web necesitan renderizar contenido del servidor o HTML estático en el tiempo de compilación. Esto puede ser una parte importante para ofrecer una experiencia razonable a los visitantes que no pueden ejecutar JavaScript.
Las justificaciones para la renderización del servidor (SSR) varían de un proyecto a otro. Algunos sitios web deben proporcionar HTML renderizado por el servidor y completamente funcional para cumplir con los lineamientos de accesibilidad. Otros prefieren ofrecer una experiencia de referencia sin JavaScript como una forma de garantizar un buen rendimiento en conexiones o dispositivos lentos.
Históricamente, ha sido difícil usar Shadow DOM en combinación con la renderización del servidor porque no había una forma integrada de expresar raíces secundarias en el HTML generado por el servidor. También hay consecuencias de rendimiento cuando se adjuntan raíces de sombra a elementos DOM que ya se renderizaron sin ellas. Esto puede provocar que el diseño cambie después de que se cargue la página o que se muestre temporalmente un destello de contenido sin diseño ("FOUC") mientras se cargan los archivos de diseño de la raíz de sombra.
El Shadow DOM declarativo (DSD) quita esta limitación y lleva Shadow DOM al servidor.
Cómo compilar un elemento raíz de sombra declarativo
Una raíz secundaria declarativa es un elemento <template>
con un atributo shadowrootmode
:
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
El analizador de HTML detecta un elemento de plantilla con el atributo shadowrootmode
y lo aplica de inmediato como la raíz de sombra de su elemento superior. Cargar el lenguaje de marcado HTML puro del ejemplo anterior genera el siguiente árbol de DOM:
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
Esta muestra de código sigue las convenciones del panel Elementos de DevTools de Chrome para mostrar contenido de Shadow DOM. Por ejemplo, el carácter ↳
representa el contenido de Light DOM con ranuras.
Esto nos brinda los beneficios del encapsulamiento y la proyección de ranuras de Shadow DOM en HTML estático. No se necesita JavaScript para producir todo el árbol, incluida la raíz de sombra.
Elementos personalizados y detección de raíces de sombras existentes
El DOM secundario declarativo se puede usar por sí solo como una forma de encapsular estilos o personalizar la posición secundaria, pero es más potente cuando se usa con elementos personalizados. Los componentes creados con elementos personalizados se actualizan automáticamente desde el HTML estático. Con la introducción del Shadow DOM declarativo, ahora es posible que un elemento personalizado tenga una shadow root antes de que se actualice.
Los elementos personalizados existen desde hace tiempo y, hasta ahora, no había razón para verificar si había un elemento raíz en sombra existente antes de crear uno con attachShadow()
. El Shadow DOM declarativo incluye un pequeño cambio que permite que los componentes existentes funcionen a pesar de esto: llamar al método attachShadow()
en un elemento con un Shadow Root declarativo existente no arrojará un error. En su lugar, se vacía y se muestra el elemento raíz de sombra declarativa. Esto permite que los componentes más antiguos que no se compilaron para el Shadow DOM declarativo sigan funcionando, ya que las raíces declarativas se conservan hasta que se crea un reemplazo imperativo.
En el caso de los elementos personalizados creados recientemente, una nueva propiedad ElementInternals.shadowRoot proporciona una forma explícita de obtener una referencia al raiz de sombra declarativa existente de un elemento, tanto abierto como cerrado. Se puede usar para buscar y usar cualquier raíz de sombra declarativa, sin dejar de recurrir a attachShadow()
en los casos en que no se proporcionó una.
Hidratación de componentes
Un elemento personalizado que se actualice desde HTML y que incluya una raíz de sombra declarativa ya tendrá esa raíz de sombra adjunta. Esto significa que el ElementInternals
del elemento tendrá una propiedad shadowRoot
ya disponible cuando se cree una instancia, sin que tu código cree una de forma explícita. Es mejor verificar ElementInternals.shadowRoot
en busca de cualquier raíz de sombra existente en el constructor de tu elemento. Si ya hay un valor, el código HTML de este componente incluye un elemento raíz de sombra declarativo. Si el valor es nulo, no había un elemento raíz de sombra declarativo en el HTML o el navegador no es compatible con el Shadow DOM declarativo.
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
const internals = supportsDeclarative ? this.attachInternals() : undefined;
const toggle = () => {
console.log("menu toggled!");
};
// check for a Declarative Shadow Root.
let shadow = internals?.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({
mode: "open",
});
shadow.innerHTML = `<button><slot></slot></button>`;
}
// in either case, wire up our event listener:
shadow.firstElementChild.addEventListener("click", toggle);
}
}
customElements.define("menu-toggle", MenuToggle);
</script>
Una sombra por raíz
Un elemento raíz de sombra declarativo solo se asocia con su elemento superior. Esto significa que las raíces de sombra siempre se encuentran en la misma ubicación que su elemento asociado. Esta decisión de diseño garantiza que las raíces de sombras se puedan transmitir como el resto de un documento HTML. También es conveniente para la autoría y la generación, ya que agregar una raíz de sombra a un elemento no requiere mantener un registro de las raíces de sombra existentes.
La desventaja de asociar raíces de sombra con su elemento superior es que no es posible que se inicialicen varios elementos desde el mismo <template>
de raíz de sombra declarativa. Sin embargo, es poco probable que esto importe en la mayoría de los casos en los que se usa el DOM secundario declarativo, ya que el contenido de cada raíz de sombra rara vez es idéntico. Si bien el HTML renderizado por el servidor suele contener estructuras de elementos repetidas, su contenido suele diferir, por ejemplo, con ligeras variaciones en el texto o los atributos. Debido a que el contenido de una raíz de sombra declarativa serializada es completamente estático, la actualización de varios elementos desde una sola raíz de sombra declarativa solo funcionaría si los elementos fueran idénticos. Por último, el impacto de las raíces de sombra similares repetidas en el tamaño de transferencia de red es relativamente pequeño debido a los efectos de la compresión.
En el futuro, es posible que se puedan volver a revisar las raíces de sombra compartidas. Si el DOM admite plantillas integradas, las raíces de sombra declarativas se podrían tratar como plantillas que se crean instancias para construir la raíz de sombra de un elemento determinado. El diseño actual de Shadow DOM declarativo permite que esta posibilidad exista en el futuro, ya que limita la asociación de la raíz de sombra a un solo elemento.
La transmisión es genial
Asociar las raíces de sombras declarativas directamente con su elemento superior simplifica el proceso de actualización y su vinculación a ese elemento. Los orígenes de sombra declarativos se detectan durante el análisis de HTML y se adjuntan de inmediato cuando se encuentra su etiqueta <template>
de apertura. El HTML analizado dentro de <template>
se analiza directamente en la raíz de la sombra, por lo que se puede "transmitir": renderizar a medida que se recibe.
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
Solo analizador
El Shadow DOM declarativo es una función del analizador HTML. Esto significa que un elemento raíz de sombra declarativo solo se analizará y adjuntará para las etiquetas <template>
con un atributo shadowrootmode
que estén presentes durante el análisis de HTML. En otras palabras, los orígenes de sombra declarativos se pueden construir durante el análisis HTML inicial:
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
Si configuras el atributo shadowrootmode
de un elemento <template>
, no se hará nada, y la plantilla seguirá siendo un elemento de plantilla normal:
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
Para evitar algunas consideraciones de seguridad importantes, los orígenes de sombra declarativos tampoco se pueden crear con APIs de análisis de fragmentos, como innerHTML
o insertAdjacentHTML()
. La única forma de analizar HTML con raíces de sombras declarativas aplicadas es usar setHTMLUnsafe()
o parseHTMLUnsafe()
:
<script>
const html = `
<div>
<template shadowrootmode="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
div.setHTMLUnsafe(html); // Shadow roots included
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>
Renderización del servidor con estilo
Las hojas de estilo intercaladas y externas son totalmente compatibles con las raíces de sombra declarativas con las etiquetas <style>
y <link>
estándar:
<nineties-button>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
Los estilos especificados de esta manera también están muy optimizados: si la misma hoja de estilo está presente en varios elementos raíz de sombra declarativos, solo se carga y analiza una vez. El navegador usa un solo CSSStyleSheet
de respaldo que comparten todas las raíces de sombra, lo que elimina la sobrecarga de memoria duplicada.
Los hojas de estilo componibles no son compatibles con el Shadow DOM declarativo. Esto se debe a que, en la actualidad, no hay forma de serializar hojas de estilo componibles en HTML ni de hacer referencia a ellas cuando se propaga adoptedStyleSheets
.
Cómo evitar el parpadeo del contenido sin diseño
Un problema potencial en los navegadores que aún no admiten el Shadow DOM declarativo es evitar el "destello de contenido sin diseño" (FOUC), en el que se muestra el contenido sin procesar de los elementos personalizados que aún no se actualizaron. Antes del Shadow DOM declarativo, una técnica común para evitar la FOUC era aplicar una regla de diseño display:none
a los elementos personalizados que aún no se habían cargado, ya que no se había conectado ni propagado su shadow root. De esta manera, el contenido no se muestra hasta que está "listo":
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
Con la introducción del DOM sombreado declarativo, los elementos personalizados se pueden renderizar o crear en HTML de modo que su contenido sombreado esté en su lugar y listo antes de que se cargue la implementación del componente del cliente:
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
En este caso, la regla "FOUC" de display:none
evitaría que se muestre el contenido del raiz de sombra declarativo. Sin embargo, quitar esa regla haría que los navegadores sin compatibilidad con el Shadow DOM declarativo mostraran contenido incorrecto o sin diseño hasta que se cargue el polyfill de Shadow DOM declarativo y convierta la plantilla de raíz de sombra en una raíz de sombra real.
Afortunadamente, esto se puede resolver en CSS si se modifica la regla de estilo de FOUC. En los navegadores que admiten Shadow DOM declarativo, el elemento <template shadowrootmode>
se convierte de inmediato en una raíz de sombra, sin dejar ningún elemento <template>
en el árbol del DOM. Los navegadores que no son compatibles con el Shadow DOM declarativo conservan el elemento <template>
, que podemos usar para evitar la FOUC:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
En lugar de ocultar el elemento personalizado aún no definido, la regla "FOUC" revisada oculta sus elementos secundarios cuando siguen un elemento <template shadowrootmode>
. Una vez que se define el elemento personalizado, la regla ya no coincide. La regla se ignora en los navegadores que admiten Shadow DOM declarativo porque el elemento secundario <template shadowrootmode>
se quita durante el análisis de HTML.
Detección de funciones y compatibilidad con navegadores
El Shadow DOM declarativo está disponible desde Chrome 90 y Edge 91, pero usaba un atributo no estándar más antiguo llamado shadowroot
en lugar del atributo shadowrootmode
estandarizado. El atributo shadowrootmode
y el comportamiento de transmisión más recientes están disponibles en Chrome 111 y Edge 111.
Como nueva API de plataforma web, el DOM sombreado declarativo aún no tiene compatibilidad generalizada en todos los navegadores. Para detectar la compatibilidad con el navegador, verifica la existencia de una propiedad shadowRootMode
en el prototipo de HTMLTemplateElement
:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Polyfill
Crear un polyfill simplificado para Shadow DOM declarativo es relativamente sencillo, ya que no es necesario que un polyfill replique perfectamente la semántica de tiempo ni las características solo de analizador que le interesan a una implementación de navegador. Para polyfillar el Shadow DOM declarativo, podemos analizar el DOM para encontrar todos los elementos <template shadowrootmode>
y, luego, convertirlos en raíces de sombra adjuntas en su elemento superior. Este proceso se puede realizar una vez que el documento esté listo o se active con eventos más específicos, como los ciclos de vida de los elementos personalizados.
(function attachShadowRoots(root) {
if (supportsDeclarativeShadowDOM()) {
// Declarative Shadow DOM is supported, no need to polyfill.
return;
}
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);