Le Shadow DOM déclaratif est une fonctionnalité standard de la plate-forme Web, compatible avec Chrome à partir de la version 90. Notez que la spécification de cette fonctionnalité a été modifiée en 2023 (y compris le changement de nom : shadowroot
a été renommé shadowrootmode
). Les versions standardisées les plus récentes de toutes les parties de la fonctionnalité ont été intégrées à la version 124 de Chrome.
Shadow DOM est l'une des trois normes de Web Components, complétée par des modèles HTML et des éléments personnalisés. Shadow DOM permet de limiter le champ d'application des styles CSS à un sous-arbre DOM spécifique et d'isoler ce sous-arbre du reste du document. L'élément <slot>
nous permet de contrôler l'emplacement où les enfants d'un élément personnalisé doivent être insérés dans son arbre d'ombre. Ces fonctionnalités combinées permettent de créer un système permettant de créer des composants autonomes et réutilisables qui s'intègrent parfaitement aux applications existantes, comme un élément HTML intégré.
Jusqu'à présent, le seul moyen d'utiliser le Shadow DOM était de construire une racine fantôme à l'aide de JavaScript:
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
Une API impérative comme celle-ci fonctionne parfaitement pour le rendu côté client: les modules JavaScript qui définissent nos éléments personnalisés créent également leurs "Shadow Roots" et définissent leur contenu. Toutefois, de nombreuses applications Web doivent afficher le contenu côté serveur ou en HTML statique au moment de la compilation. Cela peut être un élément important pour offrir une expérience raisonnable aux visiteurs qui ne sont pas forcément en mesure d'exécuter JavaScript.
Les justifications concernant le rendu côté serveur varient d'un projet à l'autre. Certains sites Web doivent fournir du code HTML côté serveur entièrement fonctionnel pour respecter les consignes d'accessibilité. D'autres choisissent de proposer une expérience de référence sans JavaScript afin de garantir de bonnes performances sur des connexions ou des appareils lents.
Auparavant, il était difficile d'utiliser le Shadow DOM en combinaison avec le rendu côté serveur, car il n'existait pas de moyen intégré d'exprimer les Shadow Roots dans le code HTML généré par le serveur. L'association de racines fantômes à des éléments DOM qui ont déjà été affichés sans ces éléments a également des répercussions sur les performances. Cela peut entraîner un décalage de la mise en page après le chargement de la page ou l'affichage temporaire d'un contenu sans style ("FOUC") pendant le chargement des feuilles de style de la racine de l'ombre.
Le Shadow DOM déclaratif (DSD) élimine cette limitation et applique le Shadow DOM au serveur.
Créer un root d'ombre déclaratif
Une racine fantôme déclarative est un élément <template>
avec un attribut shadowrootmode
:
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
Un élément de modèle comportant l'attribut shadowrootmode
est détecté par l'analyseur HTML et appliqué immédiatement en tant que racine fantôme de son élément parent. Le chargement du balisage HTML pur de l'exemple ci-dessus génère l'arborescence DOM suivante:
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
Cet exemple de code suit les conventions du panneau "Éléments" des outils pour les développeurs Chrome pour afficher le contenu Shadow DOM. Par exemple, le caractère ↳
représente le contenu Light DOM enfichable.
Cela nous permet de bénéficier des avantages de l'encapsulation et de la projection des emplacements du Shadow DOM en HTML statique. Aucun code JavaScript n'est nécessaire pour générer l'intégralité de l'arborescence, y compris le nœud racine d'ombre.
Hydratation des composants
Le DOM fantôme déclaratif peut être utilisé seul pour encapsuler des styles ou personnaliser l'emplacement des enfants, mais il est plus efficace lorsqu'il est utilisé avec des éléments personnalisés. Les composants créés à l'aide d'éléments personnalisés sont automatiquement mis à niveau à partir du code HTML statique. Avec l'introduction du Shadow DOM déclaratif, il est désormais possible de disposer d'une racine fantôme avant la mise à niveau d'un élément personnalisé.
Un élément personnalisé mis à niveau à partir d'un code HTML qui inclut une racine fantôme déclarative aura déjà cette racine fantôme associée. Cela signifie que l'élément dispose déjà d'une propriété shadowRoot
lorsqu'il est instancié, sans que votre code n'en crée explicitement une. Il est préférable de vérifier dans this.shadowRoot
si une racine d'ombre existe dans le constructeur de votre élément. Si une valeur existe déjà, le code HTML de ce composant inclut un racine d'ombre déclarative. Si la valeur est nulle, cela signifie qu'aucune racine d'ombre déclarative n'est présente dans le code HTML ou que le navigateur n'est pas compatible avec le Shadow DOM déclaratif.
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
// Detect whether we have SSR content already:
if (this.shadowRoot) {
// A Declarative Shadow Root exists!
// wire up event listeners, references, etc.:
const button = this.shadowRoot.firstElementChild;
button.addEventListener('click', toggle);
} else {
// A Declarative Shadow Root doesn't exist.
// Create a new shadow root and populate it:
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<button><slot></slot></button>`;
shadow.firstChild.addEventListener('click', toggle);
}
}
}
customElements.define('menu-toggle', MenuToggle);
</script>
Les éléments personnalisés existent depuis un certain temps, et jusqu'à présent, il n'y avait aucune raison de rechercher une racine fantôme existante avant d'en créer une à l'aide de attachShadow()
. Le Shadow DOM déclaratif inclut une légère modification qui permet aux composants existants de fonctionner malgré tout: appeler la méthode attachShadow()
sur un élément avec une racine fantôme déclarative existante ne génère pas d'erreur. Au lieu de cela, le nœud racine d'ombre déclaratif est vidé et renvoyé. Cela permet aux anciens composants non compilés pour le Shadow DOM déclaratif de continuer à fonctionner, car les racines déclaratives sont conservées jusqu'à ce qu'un remplacement impératif soit créé.
Pour les éléments personnalisés nouvellement créés, une nouvelle propriété ElementInternals.shadowRoot
permet d'obtenir explicitement une référence à la racine fantôme déclarative existante, qu'elle soit ouverte ou fermée. Cela permet de rechercher et d'utiliser une racine fantôme déclarative, tout en revenant à attachShadow()
lorsqu'aucune racine n'a été fournie.
class MenuToggle extends HTMLElement {
constructor() {
super();
const internals = this.attachInternals();
// 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.firstChild.addEventListener('click', toggle);
}
}
customElements.define('menu-toggle', MenuToggle);
Une ombre par racine
Une racine fantôme déclarative n'est associée qu'à son élément parent. Cela signifie que les racines fantômes sont toujours colocalisées avec l’élément associé. Cette décision de conception garantit que les racines fantômes peuvent être diffusées comme le reste d'un document HTML. Elle est également pratique pour la création et la génération, car l'ajout d'une racine fantôme à un élément ne nécessite pas de conserver un registre de racines fantômes existantes.
L'inconvénient d'associer des racines d'ombre à leur élément parent est qu'il est impossible d'initialiser plusieurs éléments à partir du même <template>
de racine d'ombre déclarative. Toutefois, cela est peu probable dans la plupart des cas où le Shadow DOM déclaratif est utilisé, car le contenu de chaque racine fantôme est rarement identique. Bien que le code HTML affiché par le serveur contienne souvent des structures d'éléments répétées, leur contenu diffère généralement (de légères variations dans le texte ou les attributs, par exemple). Étant donné que le contenu d'un racine d'ombre déclarative sérialisée est entièrement statique, la mise à niveau de plusieurs éléments à partir d'une seule racine d'ombre déclarative ne fonctionnerait que si les éléments étaient identiques. Enfin, l'impact de racines fantômes similaires répétées sur la taille du transfert réseau est relativement faible en raison des effets de la compression.
À l'avenir, il sera peut-être possible de revenir sur les racines d'ombre partagées. Si le DOM est compatible avec la modélisation intégrée, les racines fantômes déclaratives peuvent être traitées comme des modèles instanciés afin de construire la racine fantôme pour un élément donné. La conception actuelle du Shadow DOM déclaratif permet de le faire à l'avenir en limitant l'association de la racine fantôme à un seul élément.
Le streaming est cool
Associer des racines d'ombre déclaratives directement à leur élément parent simplifie le processus de mise à niveau et d'association à cet élément. Les racines d'ombre déclaratives sont détectées lors de l'analyse HTML et associées immédiatement lorsque leur balise <template>
d'ouverture est rencontrée. Le code HTML analysé dans <template>
est analysé directement dans la racine fantôme. Il peut donc être "en flux continu" : rendu tel qu'il est reçu.
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
Analyseur uniquement
Le Shadow DOM déclaratif est une fonctionnalité de l'analyseur HTML. Cela signifie qu'une racine fantôme déclarative ne sera analysée et associée que pour les balises <template>
ayant un attribut shadowrootmode
présentes lors de l'analyse HTML. En d'autres termes, les racines d'ombre déclaratives peuvent être créées lors de l'analyse HTML initiale :
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
Définir l'attribut shadowrootmode
d'un élément <template>
n'a aucun effet, et le modèle reste un élément de modèle ordinaire:
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
Pour éviter certains points importants de sécurité, les racines d'ombre déclaratives ne peuvent pas non plus être créées à l'aide d'API d'analyse de fragments telles que innerHTML
ou insertAdjacentHTML()
. Le seul moyen d'analyser le code HTML avec des racines fantômes déclaratives appliquées est d'utiliser setHTMLUnsafe()
ou 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>
Rendu côté serveur avec style
Les feuilles de style intégrées et externes sont entièrement compatibles avec les racines d'ombres déclaratives à l'aide des tags standards <style>
et <link>
:
<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>
Les styles spécifiés de cette façon sont également hautement optimisés: si la même feuille de style est présente dans plusieurs racines d'ombres déclaratives, elle n'est chargée et analysée qu'une seule fois. Le navigateur utilise un seul CSSStyleSheet
de sauvegarde partagé par toutes les racines d'ombre, ce qui élimine les coûts liés aux doublons de mémoire.
Les feuilles de style construesibles ne sont pas compatibles avec le Shadow DOM déclaratif. En effet, il n'existe actuellement aucun moyen de sérialiser des feuilles de style construesibles en HTML, ni de s'y référer lors du remplissage de adoptedStyleSheets
.
Comment éviter les flashs de contenu sans style
Un problème potentiel dans les navigateurs qui ne prennent pas encore en charge le DOM de l'ombre déclaratif est l'évitement du "flash de contenu non stylisé" (FOUC), où le contenu brut est affiché pour les éléments personnalisés qui n'ont pas encore été mis à niveau. Avant le Shadow DOM déclaratif, une technique courante pour éviter l'FOUC était d'appliquer une règle de style display:none
aux éléments personnalisés qui n'ont pas encore été chargés, car leur racine fantôme n'était pas associé ni rempli. Ainsi, le contenu ne s'affiche pas tant qu'il n'est pas "prêt" :
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
Avec l'introduction du Shadow DOM déclaratif, les éléments personnalisés peuvent être affichés ou créés en HTML de sorte que leur contenu d'ombre soit en place et prêt avant le chargement de l'implémentation du composant côté client :
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
Dans ce cas, la règle "FOUC" display:none
empêcherait l'affichage du contenu du root d'ombre déclaratif. Toutefois, si vous supprimez cette règle, les navigateurs qui ne prennent pas en charge le Shadow DOM déclaratif afficheront un contenu incorrect ou sans style jusqu'à ce que le polyfill du Shadow DOM déclaratif charge et convertisse le modèle de racine d'ombre en racine d'ombre réelle.
Heureusement, vous pouvez résoudre ce problème en CSS en modifiant la règle de style FOUC. Dans les navigateurs compatibles avec le Shadow DOM déclaratif, l'élément <template shadowrootmode>
est immédiatement converti en racine fantôme, ne laissant aucun élément <template>
dans l'arborescence DOM. Les navigateurs qui ne prennent pas en charge le Shadow DOM déclaratif préservent l'élément <template>
, que nous pouvons utiliser pour empêcher la FOUC:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
Au lieu de masquer l'élément personnalisé qui n'est pas encore défini, la règle "FOUC" révisée masque ses enfants lorsqu'ils suivent un élément <template shadowrootmode>
. Une fois l'élément personnalisé défini, la règle ne correspond plus. La règle est ignorée dans les navigateurs compatibles avec le Shadow DOM déclaratif, car l'enfant <template shadowrootmode>
est supprimé lors de l'analyse HTML.
Détection des fonctionnalités et compatibilité avec les navigateurs
Le Shadow DOM déclaratif est disponible depuis Chrome 90 et Edge 91, mais il utilisait un ancien attribut non standard appelé shadowroot
au lieu de l'attribut shadowrootmode
standardisé. L'attribut shadowrootmode
et le comportement de streaming plus récents sont disponibles dans Chrome 111 et Edge 111.
En tant que nouvelle API de plate-forme Web, le DOM déclaratif de l'ombre n'est pas encore largement compatible avec tous les navigateurs. La compatibilité des navigateurs peut être détectée en vérifiant l'existence d'une propriété shadowRootMode
sur le prototype de HTMLTemplateElement
:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Polyfill
La création d'un polyfill simplifié pour le Shadow DOM déclaratif est relativement simple, car un polyfill n'a pas besoin de reproduire parfaitement la sémantique temporelle ni les caractéristiques d'analyseur uniquement qui concernent une implémentation de navigateur. Pour émuler un Shadow DOM déclaratif, nous pouvons analyser le DOM pour trouver tous les éléments <template shadowrootmode>
, puis les convertir en racines d'ombre associées à leur élément parent. Ce processus peut être effectué une fois le document prêt ou déclenché par des événements plus spécifiques, tels que les cycles de vie des éléments personnalisés.
(function attachShadowRoots(root) {
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);