Cette page a été traduite à partir de l'anglais par la communauté. Vous pouvez contribuer en rejoignant la communauté francophone sur MDN Web Docs.

View in English Always switch to English

Utiliser le DOM d'ombre

Un aspect important des éléments personnalisés est l'encapsulation, car un élément personnalisé, par définition, est une fonctionnalité réutilisable : il peut être inséré dans n'importe quelle page web et on s'attend à ce qu'il fonctionne. Il est donc important que le code exécuté dans la page ne puisse pas accidentellement casser un élément personnalisé en modifiant son implémentation interne. Le DOM d'ombre (shadow DOM en anglais) permet d'associer un arbre DOM à un élément, et d'avoir l'intérieur de cet arbre inaccessible au JavaScript et au CSS exécutés dans la page.

Cet article présente les bases de l'utilisation du DOM d'ombre.

Vue de haut niveau

Cet article suppose que vous êtes déjà familier avec le concept de DOM (Document Object Model) — une structure arborescente de nœuds connectés représentant les différents éléments et chaînes de caractères apparaissant dans un document balisé (généralement un document HTML dans le cas de documents web). Par exemple, considérez le fragment HTML suivant :

html
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <title>Simple exemple de DOM</title>
  </head>
  <body>
    <section>
      <img
        src="dinosaur.png"
        alt="Un tyrannosaurus Rex rouge&nbsp;: un dinosaure bipède se tenant debout comme un humain, avec de petits bras et une large gueule à nombreuses dents tranchantes." />
      <p>
        Nous ajouterons ici un lien vers la
        <a href="https://www.mozilla.org/">page d'accueil de Mozilla</a>
      </p>
    </section>
  </body>
</html>

Ce fragment produit la structure DOM suivante :

- HTML
    - HEAD
        - META charset="utf-8"
        - TITLE
            - #text: Simple exemple de DOM
    - BODY
        - SECTION
            - IMG src="dinosaur.png" alt="Un tyrannosaurus Rex rouge : un dinosaure bipède se tenant debout comme un humain, avec de petits bras et une large gueule à nombreuses dents tranchantes."
            - P
                - #text: Ici, nous ajouterons un lien vers la
                - A href="https://www.mozilla.org/"
                    - #text: page d'accueil de Mozilla

Le DOM d'ombre permet d'associer des arbres DOM cachés à des éléments dans l'arbre DOM principal : cet arbre DOM d'ombre commence par une racine d'ombre, sous laquelle vous pouvez attacher n'importe quel élément, comme dans le DOM classique.

Version SVG du schéma montrant l'interaction entre le document, la racine d'ombre et l'hôte d'ombre.

Il existe quelques termes spécifiques au DOM d'ombre à connaître :

  • Hôte d'ombre : le nœud DOM classique auquel le DOM d'ombre est attaché.
  • Arbre d'ombre : l'arbre DOM à l'intérieur du DOM d'ombre.
  • Limite d'ombre : l'endroit où le DOM d'ombre se termine et où le DOM classique commence.
  • Racine d'ombre : le nœud racine de l'arbre d'ombre.

Vous pouvez manipuler les nœuds dans le DOM d'ombre exactement comme les nœuds classiques : par exemple, en ajoutant des enfants ou en définissant des attributs, en mettant en forme des nœuds individuels avec element.style.foo, ou en ajoutant du style à tout l'arbre DOM d'ombre à l'intérieur d'un élément <style>. La différence est qu'aucun code à l'intérieur d'un DOM d'ombre ne peut affecter quoi que ce soit à l'extérieur, ce qui permet une encapsulation pratique.

Avant que le DOM d'ombre ne soit mis à disposition des développeur·euse·s web, les navigateurs l'utilisaient déjà pour encapsuler la structure interne d'un élément. Pensez par exemple à un élément <video>, avec les contrôles par défaut du navigateur affichés. Tout ce que vous voyez dans le DOM est l'élément <video>, mais il contient une série de boutons et d'autres contrôles à l'intérieur de son DOM d'ombre. La spécification du DOM d'ombre vous permet de manipuler le DOM d'ombre de vos propres éléments personnalisés.

Héritage des attributs

L'arbre d'ombre et les éléments <slot> héritent des attributs dir et lang de leur hôte d'ombre.

Création d'un DOM d'ombre

De manière impérative avec JavaScript

La page suivante contient deux éléments, un élément HTML <div> avec un id de "host", et un élément HTML <span> contenant du texte :

html
<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>

Nous allons utiliser l'élément "host" comme hôte d'ombre. Nous appelons attachShadow() sur l'hôte pour créer le DOM d'ombre, puis nous pouvons ajouter des nœuds au DOM d'ombre comme nous le ferions dans le DOM principal. Dans cet exemple, nous ajoutons un seul élément <span> :

js
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "Je suis dans le DOM d'ombre";
shadow.appendChild(span);

Le résultat ressemble à ceci :

De manière déclarative avec HTML

Créer un DOM d'ombre via l'API JavaScript peut être une bonne option pour les applications rendues côté client. Pour d'autres applications, une interface utilisateur rendue côté serveur peut offrir de meilleures performances et donc une meilleure expérience utilisateur. Dans ces cas, vous pouvez utiliser l'élément <template> pour définir le DOM d'ombre de manière déclarative. La clé de ce comportement est l'attribut énuméré shadowrootmode, qui peut être défini sur open ou closed, les mêmes valeurs que l'option mode de la méthode attachShadow().

html
<div id="host">
  <template shadowrootmode="open">
    <span>Je suis dans le DOM d'ombre</span>
  </template>
</div>

Note : Par défaut, le contenu de <template> n'est pas affiché. Dans ce cas, puisque shadowrootmode="open" a été inclus, la racine d'ombre est rendue. Dans les navigateurs compatibles, le contenu visible à l'intérieur de cette racine d'ombre est affiché.

Après que le navigateur a analysé le HTML, il remplace l'élément <template> par son contenu enveloppé dans une racine d'ombre qui est attachée à l'élément parent, le <div id="host"> dans notre exemple. L'arbre DOM résultant ressemble à ceci (il n'y a pas d'élément <template> dans l'arbre DOM) :

- DIV id="host"
  - #shadow-root
    - SPAN
      - #text: Je suis dans le DOM d'ombre

Notez qu'en plus de shadowrootmode, vous pouvez utiliser des attributs de <template> tels que shadowrootclonable et shadowrootdelegatesfocus pour préciser d'autres propriétés de la racine d'ombre générée.

Encapsulation depuis JavaScript

Cela peut paraître peu spectaculaire pour l'instant. Voyons ce qui se passe si le code s'exécutant dans la page tente d'accéder aux éléments du DOM d'ombre.

Cette page est identique à la précédente, sauf que nous avons ajouté deux éléments <button>.

html
<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>
<br />

<button id="upper" type="button">Mettre en majuscules les éléments span</button>
<button id="reload" type="button">Recharger</button>

Cliquer sur le bouton « Mettre en majuscules les éléments span » recherche tous les éléments <span> de la page et transforme leur texte en majuscules. Cliquer sur « Recharger » recharge simplement la page, pour réessayer.

js
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "Je suis dans le DOM d'ombre";
shadow.appendChild(span);

const upper = document.querySelector("button#upper");
upper.addEventListener("click", () => {
  const spans = Array.from(document.querySelectorAll("span"));
  for (const span of spans) {
    span.textContent = span.textContent.toUpperCase();
  }
});

const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());

Si vous cliquez sur « Mettre en majuscules les éléments span », vous verrez que Document.querySelectorAll() ne trouve pas les éléments dans notre DOM d'ombre : ils sont effectivement invisibles pour le JavaScript de la page.

Element.shadowRoot et l'option « mode »

Dans l'exemple ci‑dessous, nous passons l'argument { mode: "open" } à attachShadow(). Lorsque mode est défini sur "open", le JavaScript de la page peut accéder à l'intérieur de votre DOM d'ombre via la propriété shadowRoot de l'hôte d'ombre.

Dans cet exemple, comme précédemment, le HTML contient l'hôte d'ombre, un <span> dans l'arbre DOM principal et deux boutons :

html
<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>
<br />

<button id="upper" type="button">
  Mettre en majuscules les éléments span du DOM d'ombre
</button>
<button id="reload" type="button">Recharger</button>

Cette fois, le bouton « Mettre en majuscules » utilise shadowRoot pour trouver les <span> dans le DOM :

js
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "Je suis dans le DOM d'ombre";
shadow.appendChild(span);

const upper = document.querySelector("button#upper");
upper.addEventListener("click", () => {
  const spans = Array.from(host.shadowRoot.querySelectorAll("span"));
  for (const span of spans) {
    span.textContent = span.textContent.toUpperCase();
  }
});

const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());

Cette fois, le JavaScript de la page peut accéder aux éléments internes du DOM d'ombre :

L'argument {mode: "open"} offre à la page un moyen de rompre l'encapsulation de votre DOM d'ombre. Si vous ne souhaitez pas donner cette possibilité, passez {mode: "closed"} et shadowRoot renverra null.

Cependant, cela ne doit pas être considéré comme un mécanisme de sécurité solide, car il existe des moyens de le contourner, par exemple via des extensions de navigateur s'exécutant dans la page. Il s'agit plutôt d'une indication que la page ne doit pas accéder à l'implémentation interne de votre arbre d'ombre.

Encapsulation depuis le CSS

Dans cette version de la page, le HTML est identique à l'original :

html
<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>

En JavaScript, nous créons le DOM d'ombre :

js
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "Je suis dans le DOM d'ombre";
shadow.appendChild(span);

Cette fois, nous avons du CSS ciblant les éléments <span> de la page :

css
span {
  color: blue;
  border: 1px solid black;
}

Le CSS de la page n'affecte pas les nœuds situés à l'intérieur du DOM d'ombre :

Appliquer des styles dans le DOM d'ombre

Dans cette section, nous examinons deux façons d'appliquer des styles dans un arbre de DOM d'ombre :

Dans les deux cas, les styles définis dans l'arbre du DOM d'ombre sont limités à cet arbre : tout comme les styles de la page n'affectent pas les éléments du DOM d'ombre, les styles du DOM d'ombre n'affectent pas les éléments du reste de la page.

Feuilles de style constructibles

Pour mettre en forme des éléments dans le DOM d'ombre à l'aide de feuilles de style constructibles, nous pouvons :

  1. Créer un objet CSSStyleSheet vide
  2. Définir son contenu en utilisant CSSStyleSheet.replace() ou CSSStyleSheet.replaceSync()
  3. L'ajouter à la racine d'ombre en l'assignant à ShadowRoot.adoptedStyleSheets

Les règles définies dans la CSSStyleSheet seront limitées à l'arbre du DOM d'ombre, ainsi qu'à tout autre arbre DOM auquel nous l'avons assignée.

Ici, encore une fois, le HTML contient notre hôte et un <span> :

html
<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>

Cette fois, nous créons le DOM d'ombre et lui assignons un objet CSSStyleSheet :

js
const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black;}");

const host = document.querySelector("#host");

const shadow = host.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [sheet];

const span = document.createElement("span");
span.textContent = "Je suis dans le DOM d'ombre";
shadow.appendChild(span);

Les styles définis dans l'arbre du DOM d'ombre ne s'appliquent pas au reste de la page :

Ajouter des éléments <style> dans des déclarations <template>

Une alternative à la construction d'objets CSSStyleSheet consiste à inclure un élément <style> à l'intérieur de l'élément <template> utilisé pour définir un composant Web.

Dans ce cas, le HTML inclut la déclaration <template> :

html
<template id="my-element">
  <style>
    span {
      color: red;
      border: 2px dotted black;
    }
  </style>
  <span>Je suis dans le DOM d'ombre</span>
</template>

<div id="host"></div>
<span>Je ne suis pas dans le DOM d'ombre</span>

En JavaScript, nous créons le DOM d'ombre et ajoutons le contenu du <template> :

js
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const template = document.getElementById("my-element");

shadow.appendChild(template.content);

Encore une fois, les styles définis dans le <template> ne s'appliquent qu'à l'intérieur de l'arbre du DOM d'ombre, et pas au reste de la page :

Choisir entre les options programmatiques et déclaratives

Le choix entre ces options dépend de votre application et de vos préférences personnelles.

Créer un objet CSSStyleSheet et l'assigner à la racine d'ombre via adoptedStyleSheets permet de créer une seule feuille de style et de la partager entre plusieurs arbres DOM. Par exemple, une bibliothèque de composants peut créer une seule feuille de style et la partager entre tous les éléments personnalisés de cette bibliothèque. Le navigateur analysera cette feuille une seule fois. De plus, vous pouvez modifier dynamiquement la feuille de style et propager ces changements à tous les composants qui l'utilisent.

L'approche consistant à attacher un élément <style> est idéale si vous souhaitez être déclaratif, que vous avez peu de styles et que vous n'avez pas besoin de partager les styles entre différents composants.

DOM d'ombre et éléments personnalisés

Sans l'encapsulation fournie par le DOM d'ombre, les éléments personnalisés seraient extrêmement fragiles. Il serait trop facile pour une page de casser accidentellement le comportement ou la mise en page d'un élément personnalisé en exécutant du JavaScript ou du CSS sur la page. En tant que développeur·euse d'éléments personnalisés, vous ne sauriez jamais si les sélecteurs applicables à l'intérieur de votre élément entraient en conflit avec ceux d'une page qui utilise votre élément personnalisé.

Les éléments personnalisés sont implémentés comme une classe qui étend soit la base HTMLElement, soit un élément HTML natif tel que HTMLParagraphElement. En général, l'élément personnalisé lui-même est un hôte d'ombre, et l'élément crée plusieurs éléments sous cette racine pour fournir l'implémentation interne de l'élément.

L'exemple ci-dessous crée un élément personnalisé <filled-circle> qui affiche simplement un cercle rempli d'une couleur unie.

js
class FilledCircle extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    // Créer une racine d'ombre
    // L'élément personnalisé lui-même est l'hôte d'ombre
    const shadow = this.attachShadow({ mode: "open" });

    // créer l'implémentation interne
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    const circle = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "circle",
    );
    circle.setAttribute("cx", "50");
    circle.setAttribute("cy", "50");
    circle.setAttribute("r", "50");
    circle.setAttribute("fill", this.getAttribute("color"));
    svg.appendChild(circle);

    shadow.appendChild(svg);
  }
}

customElements.define("filled-circle", FilledCircle);
html
<filled-circle color="blue"></filled-circle>

Pour d'autres exemples illustrant différents aspects de l'implémentation d'éléments personnalisés, consultez notre guide sur les éléments personnalisés.

Voir aussi