W3docs

Styles du Shadow DOM

Stylisez les web components encapsulés avec le Shadow DOM : :host, :host(), :host-context(), ::slotted(), les propriétés personnalisées CSS qui traversent la frontière, et adoptedStyleSheets.

Le Shadow DOM donne à un composant son propre arbre DOM privé et ses propres styles privés. Cette page se concentre sur la partie stylisation : comment le CSS écrit à l'intérieur d'un shadow root est encapsulé, les sélecteurs spéciaux permettant d'atteindre l'hôte et le contenu inséré via des slots (:host, :host(), :host-context(), ::slotted()), comment permettre au CSS extérieur de personnaliser un composant intentionnellement (propriétés personnalisées), et les deux façons d'attacher une feuille de style (<style> vs adoptedStyleSheets).

Si le Shadow DOM est nouveau pour vous, lisez d'abord JavaScript Shadow DOM, et consultez Web Components pour avoir une vue d'ensemble de la place des shadow roots.

Pourquoi les styles sont encapsulés

La promesse fondamentale du Shadow DOM est une frontière de style bidirectionnelle :

  • Le CSS extérieur ne pénètre pas à l'intérieur. Une règle globale comme p { color: red } ne touchera pas un <p> à l'intérieur d'un shadow root. C'est ce qui rend les composants utilisables en toute sécurité dans n'importe quelle page.
  • Le CSS intérieur ne s'échappe pas vers l'extérieur. Les styles d'un shadow root s'appliquent uniquement à l'intérieur de ce root, vous pouvez donc utiliser des sélecteurs courts et génériques (button, p, .title) sans craindre de conflits avec la page hôte.

Cela diffère du modèle classique des styles et classes, où chaque sélecteur est en compétition dans un seul scope global. À l'intérieur d'un shadow root, le scope est la règle par défaut.

Création d'un shadow root

Pour commencer, attachez un shadow root à un élément hôte. Tout ce que vous y placez — balisage et CSS — est encapsulé.

<body>
  <div id="my-element"></div>
    <script>
      // Creating Shadow DOM
      const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });
    
      // Styling Shadow DOM
      shadowRoot.innerHTML = `
        <p>A simple shadow root content.</p>
      `;
    </script>
</body>

Ici, nous attachons un shadow root avec la méthode attachShadow() et définissons son mode à 'open', ce qui vous permet de récupérer le root ultérieurement via element.shadowRoot. ('closed' le masque aux scripts extérieurs, mais n'apporte pas de véritable sécurité.)

Ajout de styles délimités avec <style>

La façon la plus simple de styliser un shadow root est d'y placer un élément <style>. Ces règles s'appliquent uniquement à l'intérieur du root — et les règles de la page restent à l'extérieur.

<div id="my-element">
  <!-- Shadow DOM content -->
</div>

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

  shadowRoot.innerHTML = `
    <style>
      /* Scoped styles */
      :host {
        display: block;
        border: 2px solid #333;
        padding: 10px;
      }

      p {
        color: blue;
      }
    </style>

    <p>This paragraph is styled within the Shadow DOM.</p>
  `;
</script>

La règle p { color: blue } ne colore que le paragraphe à l'intérieur de ce root — un <p> ailleurs sur la page n'est pas affecté. La règle :host (ci-dessous) stylise l'élément hôte lui-même.

Cibler l'hôte : :host, :host(), :host-context()

Un shadow root ne peut pas sélectionner son élément hôte avec un sélecteur normal, car l'hôte se trouve en dehors du root. Trois pseudo-classes permettent de faire le lien :

SélecteurCorrespond àUtilisation
:hostL'élément hôte, toujoursStyles de base du composant (display, padding, boîte).
:host(<sélecteur>)L'hôte uniquement quand il correspond à <sélecteur>Variantes et états pilotés par des attributs/classes/pseudo-classes, ex. :host([disabled]), :host(:hover).
:host-context(<sélecteur>)L'hôte quand un ancêtre correspond à <sélecteur>S'adapter au contexte, ex. :host-context(.dark-theme).
<div class="dark-theme">
  <fancy-box disabled>Boxed content</fancy-box>
</div>

<script>
  class FancyBox extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            display: block;
            padding: 12px;
            border: 2px solid #007bff;
          }
          /* Variant: applies only when the host has [disabled] */
          :host([disabled]) {
            opacity: 0.5;
            pointer-events: none;
          }
          /* Context: applies when any ancestor has .dark-theme */
          :host-context(.dark-theme) {
            background: #1e1e1e;
            color: #fff;
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('fancy-box', FancyBox);
</script>

Comme l'élément hôte possède l'attribut [disabled] et se trouve à l'intérieur de .dark-theme, les trois règles s'appliquent : il s'affiche sombre, estompé et non interactif.

Avertissement

:host-context() dispose d'un support navigateur limité (pas de Firefox au moment de la rédaction). Préférez une propriété personnalisée CSS ou un attribut explicite sur l'hôte pour une compatibilité plus large.

Styliser le contenu inséré via des slots avec ::slotted()

Le contenu que l'utilisateur passe dans votre composant se trouve dans le light DOM et est rendu via un <slot>. Ce contenu appartient toujours à la page, donc les styles de la page sont prioritaires — mais vous pouvez quand même l'atteindre depuis l'intérieur du shadow root avec ::slotted().

Une limitation importante : ::slotted() ne correspond qu'aux nœuds insérés de premier niveau, pas à leurs descendants. ::slotted(span) fonctionne ; ::slotted(div span) ne fonctionne pas.

<body>
<script>
  class CustomButton extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });

      shadowRoot.innerHTML = `
        <style>
          :host {
            display: inline-block;
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
          }

          :host(:hover) {
            background-color: #0056b3;
          }

          button {
            font-weight: bold;
            border: none;
            background: none;
            color: inherit;
            cursor: inherit;
            padding: 0;
          }

          /* Styling slotted content */
          ::slotted(span) {
            font-style: italic;
            text-decoration: underline;
          }
        </style>

        <button>
          <slot></slot>
        </button>
      `;
    }
  }

  customElements.define('custom-button', CustomButton);
</script>

<!-- Test custom-button with slotted content -->
<custom-button id="my-button">Click <span>here</span></custom-button>
</body>

Ici, ::slotted(span) cible le <span> passé en tant que contenu de slot, en le mettant en italique et en le soulignant, tandis que le texte "Click" environnant reste intact.

Permettre à la page de personnaliser un composant : les propriétés personnalisées CSS

L'encapsulation est efficace, mais peut ressembler à un mur : la page hôte ne peut pas atteindre l'intérieur pour recolorer un bouton. La solution prévue est d'utiliser les propriétés personnalisées CSS (variables) — c'est le seul élément qui traverse la frontière du shadow. Le composant lit une variable avec var() en fournissant une valeur de repli ; la page définit cette variable depuis l'extérieur.

<style>
  /* The page customizes the component from outside the boundary */
  theme-button {
    --btn-bg: #28a745;
    --btn-bg-hover: #1e7e34;
  }
</style>

<theme-button>Save</theme-button>

<script>
  class ThemeButton extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            /* var(--name, fallback): fallback is used if the page sets nothing */
            background: var(--btn-bg, #007bff);
            color: #fff;
            padding: 8px 16px;
            display: inline-block;
            cursor: pointer;
          }
          :host(:hover) {
            background: var(--btn-bg-hover, #0056b3);
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('theme-button', ThemeButton);
</script>

Le bouton s'affiche en vert car la page a défini --btn-bg. Supprimez ces deux déclarations et il revient au bleu (#007bff). C'est la façon la plus propre d'exposer une API de thème tout en gardant les détails internes du composant privés.

<style> vs adoptedStyleSheets

Placer une balise <style> dans le innerHTML de chaque instance fonctionne, mais duplique le texte CSS pour chaque composant sur la page et oblige le navigateur à le re-analyser à chaque fois. Pour les composants créés de nombreuses fois, partagez un seul CSSStyleSheet analysé entre les roots avec adoptedStyleSheets.

<my-badge>New</my-badge>
<my-badge>Beta</my-badge>

<script>
  // Parsed once, reused by every instance
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    :host {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 999px;
      background: #007bff;
      color: #fff;
      font-size: 12px;
    }
  `);

  class MyBadge extends HTMLElement {
    constructor() {
      super();
      const root = this.attachShadow({ mode: 'open' });
      root.adoptedStyleSheets = [sheet]; // adopt the shared sheet
      root.innerHTML = `<slot></slot>`;
    }
  }
  customElements.define('my-badge', MyBadge);
</script>

Quand utiliser lequel :

  • <style> à l'intérieur du root — le plus simple, sans code supplémentaire, convient pour les composants ponctuels ou les petites démonstrations.
  • adoptedStyleSheets — préférable quand le même composant apparaît de nombreuses fois : une feuille de style partagée et constructible signifie moins de mémoire et une instanciation plus rapide. Vous pouvez également mettre à jour la feuille à l'exécution (sheet.replaceSync(...)) et chaque root qui l'adopte reflète immédiatement le changement.

Conclusion

Le style avec le Shadow DOM repose sur quelques idées fondamentales : les styles sont délimités dans les deux sens, vous atteignez l'hôte avec :host / :host() / :host-context(), vous atteignez le contenu projeté avec ::slotted(), vous exposez la thématisation via les propriétés personnalisées CSS, et vous attachez le CSS soit de manière inline avec <style>, soit efficacement avec adoptedStyleSheets. Ensemble, ils vous permettent de livrer des composants qui s'affichent correctement partout tout en restant personnalisables selon vos conditions.

Pour aller plus loin, consultez Shadow DOM Slots & Composition pour comprendre comment le contenu inséré via des slots est assemblé, et Web Components pour combiner les shadow roots avec des éléments personnalisés et des templates.

Pratique

Pratique
Quelle fonctionnalité du Shadow DOM permet aux développeurs de styliser le contenu projeté dans les éléments personnalisés ?
Quelle fonctionnalité du Shadow DOM permet aux développeurs de styliser le contenu projeté dans les éléments personnalisés ?
Was this page helpful?