Le DOM est une interface de programmation (API) pour le code d’une page, qui représente la page comme une structure d’objets en arborescence.
Chaque élément HTML (par exemple, <p>, <div>, <img>), chaque attribut et chaque morceau de texte est un « nœud » distinct dans cette arborescence. En utilisant JavaScript, nous pouvons interagir avec ces nœuds pour modifier dynamiquement la page : changer du texte, ajouter des styles, créer de nouveaux éléments ou supprimer ceux qui existent. Essentiellement, le DOM est un modèle « vivant » du document avec lequel le code interagit.
Mais cette ouverture a un inconvénient. Lorsque nous créons un composant complexe et réutilisable (par exemple, un lecteur vidéo personnalisé ou un widget de calendrier), sa structure interne et ses styles deviennent vulnérables. Les styles CSS de la page principale peuvent accidentellement « fuiter » dans le composant et casser son apparence. De même, le code JavaScript de la page peut involontairement modifier les éléments internes du composant, perturbant sa logique.
Pour résoudre ce problème, le Shadow DOM existe.
Essentiellement, le Shadow DOM est un « DOM dans le DOM ». C’est une arborescence d’éléments cachée, attachée à un élément de page normal (appelé « l’hôte ») mais isolée du DOM principal. Il permet au développeur de créer une frontière étanche autour de la structure interne d’un composant, le protégeant du monde extérieur.
Le Shadow DOM vous permet d’attacher des arborescences DOM cachées à des éléments de l’arborescence DOM normale. Cette arborescence fantôme commence par une racine fantôme (shadow root), sous laquelle n’importe quel élément peut être ajouté, tout comme dans le DOM normal.
Il y a plusieurs termes clés liés au Shadow DOM que vous devriez connaître. L’hôte fantôme (Shadow host) est le nœud DOM normal auquel un Shadow DOM est attaché. L’arborescence DOM à l’intérieur du Shadow DOM est connue sous le nom d’arborescence fantôme (Shadow tree). Le point où le Shadow DOM se termine et où le DOM normal commence est appelé la frontière fantôme (Shadow boundary). Enfin, le nœud racine de l’arborescence fantôme est la racine fantôme (Shadow root).

Vous pouvez manipuler les nœuds à l’intérieur du Shadow DOM comme des nœuds normaux. La différence est qu’aucun code à l’intérieur du Shadow DOM ne peut affecter quoi que ce soit à l’extérieur, garantissant une encapsulation fiable.
Avant que le Shadow DOM ne soit disponible pour les développeurs web, les navigateurs l’utilisaient déjà pour encapsuler la structure interne des éléments standards. Par exemple, l’élément <video> avec ses contrôles. Tout ce que vous voyez dans le DOM est la balise <video>, mais elle contient plusieurs boutons et autres contrôles à l’intérieur de son Shadow DOM.
Création du Shadow DOM
Le Shadow DOM peut être créé soit de manière impérative en utilisant JavaScript, soit de manière déclarative directement en HTML.
De manière impérative avec JavaScript
Cette méthode est parfaite pour les applications rendues côté client. Nous sélectionnons l’élément hôte et appelons la méthode attachShadow() sur celui-ci.
<!-- Balisage HTML -->
<div id="host"></div>
<span>Je ne suis pas dans le Shadow DOM</span>
// Trouver l'hôte et attacher le Shadow DOM
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
// Créer et ajouter des éléments à l'arborescence fantôme
const span = document.createElement("span");
span.textContent = "Je suis dans le Shadow DOM";
shadow.appendChild(span);
Le résultat sur la page ressemblera à ceci :
Je suis dans le Shadow DOM
Je ne suis pas dans le Shadow DOM
De manière déclarative avec le HTML
Pour les applications où le rendu côté serveur est important, vous pouvez définir le Shadow DOM de manière déclarative en utilisant un élément <template> avec l’attribut shadowrootmode.
<div id="host">
<template shadowrootmode="open">
<p>Ce paragraphe est à l'intérieur du Shadow DOM.</p>
<style>
p { color: red; } /* Ces styles seront isolés */
</style>
</template>
</div>
Lorsque le navigateur traite ce code, il crée automatiquement une racine fantôme pour le <div> et place le contenu du <template> à l’intérieur. La balise <template> elle-même disparaît de l’arborescence DOM principale.
Encapsulation : protection contre JavaScript et CSS
Le principal avantage du Shadow DOM est l’isolation. Voyons comment cela fonctionne.
Encapsulation JavaScript
Ajoutez un bouton qui essaie de modifier tous les éléments <span> sur la page.
// ... Code de création du Shadow DOM ...
const upper = document.querySelector("#upper-button");
upper.addEventListener("click", () => {
// Ce sélecteur recherche dans tout le document
const spans = document.querySelectorAll("span");
for (const span of spans) {
span.textContent = span.textContent.toUpperCase();
}
});
Lorsque l’on clique sur le bouton, seul le <span> dans le document principal change. L’élément à l’intérieur du Shadow DOM reste intact car document.querySelectorAll() ne peut pas « regarder » au-delà de la frontière fantôme.
Accéder au Shadow DOM : propriété shadowRoot et arborescences imbriquées
Lorsque nous appelons host.attachShadow({ mode: "open" }), nous créons un Shadow DOM « ouvert ». Cela signifie que nous pouvons accéder à son contenu depuis l’extérieur via host.shadowRoot.
// Trouver les spans uniquement à l'intérieur de l'arborescence fantôme d'un hôte spécifique
const spansInShadow = host.shadowRoot.querySelectorAll("span");
Si mode: "closed" est utilisé, host.shadowRoot renvoie null, et l’accès externe à l’arborescence fantôme est bloqué. Ce n’est pas un mécanisme de sécurité strict, mais plutôt une convention pour les développeurs indiquant que les internes du composant ne devraient pas être modifiés.
Travailler avec des arborescences fantômes imbriquées
Dans les architectures de composants complexes, un élément personnalisé peut en contenir d’autres, chacun avec son propre Shadow DOM. Pour accéder à un élément dans une arborescence fantôme profondément imbriquée, vous devez traverser chaque shadowRoot séquentiellement.
La structure pourrait être un composant <nmbrs-form> (le formulaire principal), qui contient un <div> contenant un <nmbrs-button> (un bouton personnalisé), et à l’intérieur de ce bouton personnalisé se trouve un véritable <button> HTML.
Pour accéder à ce bouton depuis le contexte global, le chemin est :
// 1. Trouver le composant racine dans le document principal
const formComponent = document.querySelector('nmbrs-form');
// 2. Entrer dans son arborescence fantôme
const shadowRoot1 = formComponent.shadowRoot;
// 3. Trouver le composant bouton imbriqué
const buttonComponent = shadowRoot1.querySelector('div div.btn-container nmbrs-button');
// 4. Entrer dans l'arborescence fantôme de ce composant
const shadowRoot2 = buttonComponent.shadowRoot;
// 5. Finalement, trouver l'élément bouton
const finalButton = shadowRoot2.querySelector('button#button');
En une seule chaîne :
const button = document.querySelector('nmbrs-form').shadowRoot
.querySelector('div div.btn-container nmbrs-button').shadowRoot
.querySelector('button#button');
Cette longue chaîne démontre la puissance de l’encapsulation : pour atteindre les détails internes, vous devez passer explicitement par chaque « frontière ». Cela rend le code plus prévisible et protège les composants contre les modifications accidentelles.
Encapsulation CSS
Les styles définis sur la page principale n’affectent pas les éléments à l’intérieur du Shadow DOM.
/* Ce style s'applique uniquement aux spans dans le document principal */
span {
color: blue;
border: 1px solid black;
}
Le <span> à l’intérieur de l’arborescence fantôme n’obtiendra pas ces styles. Cela résout le problème des conflits de style accidentels.
Styliser à l’intérieur du Shadow DOM
Les styles définis à l’intérieur de l’arborescence fantôme, à leur tour, n’affectent pas la page principale. Il y a deux manières principales de les ajouter.
1. Feuilles de style constructibles (Constructable Stylesheets)
Cette méthode permet de créer un objet CSSStyleSheet en JavaScript et de l’appliquer à une ou plusieurs arborescences fantômes. C’est efficace si vous avez des styles partagés pour de nombreux composants.
const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black; }");
const shadow = host.attachShadow({ mode: "open" });
// Appliquer la feuille de style à la racine fantôme
shadow.adoptedStyleSheets = [sheet];
2. L’élément <style>
Un moyen simple et déclaratif est de placer une balise <style> directement à l’intérieur de l’arborescence fantôme (souvent à l’intérieur d’un <template>).
<template id="my-element">
<style>
span {
color: red;
border: 2px dotted black;
}
</style>
<span>Je suis dans le Shadow DOM</span>
</template>
Shadow DOM et Éléments Personnalisés : une combinaison parfaite
Toute la puissance du Shadow DOM se révèle lors de la création d’Éléments Personnalisés (Custom Elements). Sans encapsulation, ils seraient extrêmement fragiles.
Un élément personnalisé est une classe qui étend HTMLElement. Typiquement, l’élément lui-même agit comme l’hôte fantôme, et sa structure interne est créée à l’intérieur de l’arborescence fantôme.
Exemple d’un composant simple <filled-circle> :
class FilledCircle extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
// Créer l'implémentation interne (par ex., un cercle 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");
// La couleur provient de l'attribut de l'hôte
circle.setAttribute("fill", this.getAttribute("color"));
svg.appendChild(circle);
shadow.appendChild(svg);
}
}
customElements.define("filled-circle", FilledCircle);
Maintenant, nous pouvons l’utiliser en HTML comme une balise normale sans nous soucier de sa structure interne :
<filled-circle color="blue"></filled-circle>
<filled-circle color="green"></filled-circle>
Chacun de ces composants sera entièrement encapsulé et protégé de la page externe.