W3docs

JavaScript Shadow DOM

Le Shadow DOM permet d'attacher un arbre DOM encapsulé à un élément afin que son balisage, ses styles et ses scripts restent isolés du reste de la page. Apprenez à créer des racines shadow ouvertes et fermées, à délimiter les styles, à utiliser les slots et à construire des composants réutilisables.

Le Shadow DOM est un bloc de construction fondamental des Web Components, qui vous permet d'attacher un arbre DOM encapsulé et une portée de style isolée à un élément. Ce guide explique ce qu'est le Shadow DOM, pourquoi il est important, comment créer des racines shadow ouvertes et fermées, délimiter les styles, projeter du contenu avec des slots, et assembler le tout au sein d'un élément personnalisé réutilisable.

Qu'est-ce que le Shadow DOM ?

Le Shadow DOM vous permet d'attacher un sous-arbre DOM distinct et masqué à un élément. Le balisage et les styles à l'intérieur de ce sous-arbre sont encapsulés : ils ne s'échappent pas vers l'extérieur, et les styles globaux n'y pénètrent pas. Cela résout l'un des problèmes les plus anciens du développement front-end — les collisions de CSS global et d'identifiants entre composants.

Quelques termes méritent d'être définis dès le départ :

  • Shadow host — l'élément ordinaire auquel l'arbre shadow est attaché.
  • Shadow root — le nœud racine de l'arbre masqué, retourné par attachShadow().
  • Shadow tree — le DOM à l'intérieur de la shadow root.
  • Light DOM — les enfants ordinaires de l'élément, écrits dans un balisage normal ; ils peuvent être projetés dans l'arbre shadow à travers des slots.

Le navigateur lui-même utilise le Shadow DOM en interne : les contrôles d'un élément <video> ou <input type="range"> vivent dans un arbre shadow inaccessible, ce qui explique précisément pourquoi leurs composants internes n'entrent jamais en conflit avec votre CSS.

Dans l'exemple ci-dessous, deux éléments partagent la classe shadow-box, mais chacun conserve son propre style car l'un vit dans le document principal et l'autre à l'intérieur d'une shadow root.

<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

Dans cet exemple, deux éléments portent le même nom de classe shadow-box. Le premier élément est stylisé par le CSS du document principal, tandis que le second est stylisé par le CSS du Shadow DOM. Comme vous pouvez le constater, les styles définis dans le Shadow DOM n'affectent pas les éléments du document principal, et vice versa. Cela illustre l'encapsulation offerte par le Shadow DOM, qui vous permet de créer des composants isolés et réutilisables sans vous soucier des conflits de styles.

Création d'une Shadow Root

Pour créer une shadow root, utilisez la méthode attachShadow sur un élément. La shadow root peut être soit open (ouverte), soit closed (fermée). Une shadow root open est accessible depuis du JavaScript extérieur à l'arbre shadow, tandis qu'une shadow root closed ne l'est pas.

Shadow Root ouverte

Une shadow root ouverte permet l'accès et la manipulation depuis du JavaScript externe. Dans l'exemple ci-dessous, nous manipulons le contenu textuel à l'intérieur de la shadow root après sa création.

<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

Dans cet exemple, un bouton permet de modifier le contenu du Shadow DOM. Puisque la shadow root est ouverte, nous pouvons accéder à son contenu et le manipuler depuis le document principal.

Shadow Root fermée

Une shadow root fermée restreint l'accès depuis les scripts externes, offrant une meilleure encapsulation. Dans l'exemple ci-dessous, nous tentons de manipuler le contenu textuel à l'intérieur de la shadow root après sa création, mais cela est impossible car elle est closed.

<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

Ici, la tentative échoue parce que la shadow root est fermée : closedShadowHost.shadowRoot retourne null, donc null.querySelector(...) lève une TypeError et le bloc catch s'exécute. La référence retournée par attachShadow({ mode: 'closed' }) est le seul moyen d'accéder à cet arbre, donc gardez-la privée à l'intérieur de votre composant.

Une idée reçue courante est que closed rend un composant véritablement sécurisé — ce n'est pas le cas. Cela décourage simplement l'accès externe occasionnel ; un code qui détient la référence originale à la racine (ou qui intercepte attachShadow) peut toujours y accéder. Utilisez open sauf si vous avez une raison concrète de masquer les composants internes, car open facilite grandement le débogage et les tests.

Aspectmode: 'open'mode: 'closed'
host.shadowRootRetourne la shadow rootRetourne null
Accès externeAutorisé via host.shadowRootUniquement via la référence sauvegardée
Utilisation typiqueLa plupart des composants, débogage facileMasquer les composants internes aux scripts de la page
Inspection dans les DevToolsEntièrement visibleVisible, mais plus difficile à scripter

Styles dans le Shadow DOM

Avertissement

Lors de l'implémentation du JavaScript Shadow DOM, veillez à assurer une encapsulation correcte pour éviter les conflits de styles ou de scripts non intentionnels.

Les styles définis à l'intérieur d'une shadow root n'affectent pas les éléments en dehors, et vice versa. Cette encapsulation est bénéfique pour créer des composants réutilisables.

<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

Dans cet exemple, deux éléments portent le nom de classe styled-box. Le premier élément est stylisé par le CSS du document principal, tandis que le second est stylisé par le CSS du Shadow DOM. Les styles définis dans le Shadow DOM n'affectent pas les éléments du document principal, et les styles définis dans le document principal n'affectent pas les éléments du Shadow DOM. Cela montre comment le Shadow DOM encapsule les styles, garantissant l'absence de conflits entre les styles du composant et les styles globaux.

Sélecteurs spéciaux pour le Shadow DOM

L'encapsulation ne signifie pas un isolement total. Trois sélecteurs vous offrent des points d'accroche contrôlés à travers la frontière :

  • :host — utilisé à l'intérieur de l'arbre shadow pour styliser l'élément host lui-même. :host(.active) correspond uniquement lorsque le host porte cette classe.
  • ::slotted(selector) — utilisé à l'intérieur de l'arbre shadow pour styliser les nœuds du light DOM projetés dans un slot. Il ne peut cibler que les éléments slottés de premier niveau, pas leurs descendants.
  • ::part(name) — utilisé dans le document extérieur pour styliser un élément interne que le composant expose explicitement avec un attribut part="name". C'est la manière sanctionnée de permettre aux consommateurs de thématiser un composant sans accéder à ses composants internes.
<body>
  <div id="theme-host">
    <span>Projected from the light DOM</span>
  </div>

  <style>
    /* Outer page can only reach parts the component exposes */
    #theme-host::part(label) {
      text-decoration: underline;
    }
  </style>

  <script>
    const host = document.getElementById('theme-host');
    const root = host.attachShadow({ mode: 'open' });

    root.innerHTML = `
      <style>
        :host { display: block; padding: 10px; border: 2px solid teal; }
        .label { font-weight: bold; color: teal; }
        ::slotted(span) { color: crimson; }
      </style>
      <div class="label" part="label">Styled with :host and ::part</div>
      <slot></slot>
    `;
  </script>
</body>

La règle :host encadre l'ensemble du composant, .label est interne et privé, ::slotted(span) colorise le texte du light DOM projeté, et ::part(label) permet à la page extérieure de souligner l'étiquette qu'elle a reçu la permission de thématiser. Tout ce qui n'est pas exposé en tant que part reste inaccessible depuis l'extérieur.

Slots : contenu du Light DOM dans le Shadow DOM

Les slots permettent aux développeurs de passer du contenu du light DOM (DOM ordinaire) dans un Shadow DOM, rendant ainsi le Shadow DOM plus flexible et réutilisable.

<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

Dans cet exemple, l'élément <slot> est utilisé pour passer du contenu du light DOM vers le Shadow DOM. L'attribut slot sur l'élément span correspond à l'attribut name de l'élément slot dans le Shadow DOM, permettant ainsi au contenu du span d'être projeté dans le Shadow DOM.

Interaction JavaScript avec le Shadow DOM

Interagir avec le Shadow DOM via JavaScript nécessite de comprendre les frontières d'encapsulation. La manipulation directe à l'intérieur de la shadow root est simple, mais l'interaction externe demande une gestion minutieuse.

Accès aux éléments du Shadow DOM

Pour accéder aux éléments à l'intérieur d'un Shadow DOM, utilisez la propriété shadowRoot.

<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.querySelector('#shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

Dans cet exemple, nous accédons au bouton à l'intérieur du Shadow DOM en utilisant querySelector sur la shadow root. Puisque la shadow root est ouverte, nous pouvons attacher des écouteurs d'événements et manipuler les éléments directement depuis le document principal.

Reciblage des événements

Les événements qui remontent depuis un arbre shadow sont reciblés : pour les écouteurs dans le document extérieur, event.target pointe vers le shadow host, et non vers l'élément interne qui a réellement été cliqué. Cela maintient la structure interne privée. À l'intérieur de l'arbre shadow, la cible réelle est toujours disponible via event.composedPath()[0] ou event.target.

<div id="event-host"></div>

<script>
  const host = document.getElementById('event-host');
  const root = host.attachShadow({ mode: 'open' });
  root.innerHTML = '<button id="inner">Click me</button>';

  // Listener in the OUTER document
  document.addEventListener('click', (e) => {
    console.log('Outer target:', e.target.id || e.target.tagName);
    console.log('Real target:', e.composedPath()[0].id);
  });
</script>

En cliquant sur le bouton, on obtient Outer target: event-host (reciblé vers le host) mais Real target: inner depuis composedPath(). Notez que les événements personnalisés ne franchissent la frontière shadow que s'ils sont créés avec { bubbles: true, composed: true }.

Exemples pratiques de Shadow DOM

Créer un Web Component réutilisable

La création d'un web component réutilisable avec le Shadow DOM implique de définir un élément personnalisé et d'y attacher une shadow root.

<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

Dans cet exemple, un élément personnalisé <custom-card> est créé avec un Shadow DOM. Le Shadow DOM encapsule les styles et la structure du composant, le rendant réutilisable sans se soucier des conflits de styles avec le document principal. Associer le Shadow DOM aux éléments personnalisés et à l'élément <template> est la recette standard pour des Web Components en production.

Intégration avec les frameworks

Le Shadow DOM peut être utilisé de manière transparente avec les frameworks JavaScript modernes tels que React, Angular et Vue.

Exemple avec React

Dans React, vous pouvez attacher un Shadow DOM à un élément conteneur de la manière suivante :

<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useLayoutEffect } = React;

    const CustomCard = ({ title, content }) => {
      const cardRef = useRef(null);

      useLayoutEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${content}</div>
            </div>
          `;
        }
      }, [title, content]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World" content="This is content inside the shadow DOM.">
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

Dans cet exemple, un composant React CustomCard est créé, qui attache un Shadow DOM à un div ordinaire. Le Shadow DOM garantit que les styles et la structure du composant sont encapsulés, assurant une intégration transparente avec React.

Quand utiliser le Shadow DOM

Le Shadow DOM n'est pas nécessaire pour chaque composant ; pesez-en les avantages et les inconvénients :

  • Utilisez-le lorsque vous livrez un widget autonome et réutilisable — en particulier un widget utilisé sur des pages dont vous ne contrôlez pas le CSS global (intégrations, primitives de système de design, widgets tiers).
  • Évitez-le lorsque votre composant vit entièrement dans une application qui délimite déjà les styles (CSS Modules, styles scoped, BEM) et que vous souhaitez que la thématisation globale s'applique librement.
  • Méfiez-vous de ces pièges courants :
    • Les feuilles de style globales et les polices ne se propagent pas automatiquement ; déclarez ce dont vous avez besoin à l'intérieur de la racine, ou transmettez les valeurs avec des propriétés personnalisées CSS (--my-color), qui traversent bien la frontière.
    • Les éléments associés à des formulaires nécessitent un câblage supplémentaire (l'API ElementInternals) pour participer à un <form> environnant.
    • Le rendu côté serveur des arbres shadow nécessite le Declarative Shadow DOM (<template shadowrootmode="open">).
Info

Règle générale : préférez mode: 'open' et exposez les points d'accroche de thématisation avec ::part() et les propriétés personnalisées CSS. Utilisez closed uniquement lorsque masquer les composants internes est une véritable exigence.

Conclusion

Maîtriser le Shadow DOM est essentiel pour le développement web moderne, offrant une encapsulation et une réutilisabilité puissantes. En comprenant et en mettant en œuvre les concepts et exemples présentés, vous pouvez créer des composants robustes et isolés qui améliorent la maintenabilité et l'évolutivité de vos applications web.

Ce guide complet devrait servir de base solide pour explorer et utiliser le Shadow DOM dans vos projets. Que vous construisiez de simples widgets ou des applications complexes, le Shadow DOM offre l'encapsulation et la flexibilité nécessaires pour garantir que vos composants restent isolés et gérables.

Pratique

Pratique
Quelle méthode est utilisée pour créer une shadow root en JavaScript ?
Quelle méthode est utilisée pour créer une shadow root en JavaScript ?
Pratique
Que retourne host.shadowRoot lorsque la racine a été créée avec mode: 'closed' ?
Que retourne host.shadowRoot lorsque la racine a été créée avec mode: 'closed' ?
Pratique
Quel sélecteur permet au document extérieur de styliser uniquement les parties internes qu'un composant expose explicitement ?
Quel sélecteur permet au document extérieur de styliser uniquement les parties internes qu'un composant expose explicitement ?
Was this page helpful?