Shadow DOM 201

CSS et style

Cet article décrit tout ce que vous pouvez faire avec Shadow DOM. Il s'appuie sur les concepts abordés dans la section Principes de base du Shadow DOM. Si vous souhaitez une introduction, lisez cet article.

Introduction

Soyons réalistes. Le balisage sans style n'est pas sexy. Heureusement pour nous, les personnes brillantes à l'origine de Web Components l'ont anticipé et ne nous ont pas empêchés. Le module de champ d'application CSS définit de nombreuses options permettant d'appliquer des styles au contenu dans une arborescence fantôme.

Encapsulation du style

La limite des ombres est l'une des principales fonctionnalités de Shadow DOM. Elle présente de nombreuses propriétés intéressantes, mais l'une des meilleures est qu'elle fournit sans frais l'encapsulation des styles. En d'autres termes:

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

Voici deux observations intéressantes concernant cette démonstration:

  • Il y a d'autres h3 sur cette page, mais le seul qui correspond au sélecteur h3, et donc stylisé en rouge, est celui qui se trouve dans ShadowRoot. Là encore, les styles restreints sont utilisés par défaut.
  • Les autres règles de style définies sur cette page qui ciblent le format h3 ne s'affichent pas dans mon contenu. En effet, les sélecteurs ne dépassent pas la limite de l'ombre.

La morale de l'histoire ? Nous avons une encapsulation de style du monde extérieur. Merci à Shadow DOM !

Appliquer un style à l'élément hôte

:host vous permet de sélectionner et de styliser l'élément hébergeant un arbre ombré:

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

L'un des pièges est que les règles de la page parente ont une spécificité plus élevée que les règles :host définies dans l'élément, mais une spécificité inférieure à celle d'un attribut style défini sur l'élément hôte. Cela permet aux utilisateurs de remplacer votre style depuis l'extérieur. :host ne fonctionne également que dans le contexte d'un ShadowRoot. Vous ne pouvez donc pas l'utiliser en dehors de Shadow DOM.

La forme fonctionnelle de :host(<selector>) vous permet de cibler l'élément hôte s'il correspond à un <selector>.

Exemple – Établir une correspondance uniquement si l'élément lui-même possède la classe .different (par exemple, <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

Réagir aux états des utilisateurs

:host est souvent utilisé lorsque vous créez un élément personnalisé et que vous souhaitez réagir à différents états utilisateur (:hover, :focus, :active, etc.).

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

Appliquer un thème à un élément

La pseudo-classe :host-context(<selector>) correspond à l'élément hôte si lui-même ou l'un de ses ancêtres correspond à <selector>.

:host-context() est couramment utilisé pour thématiser un élément en fonction de son environnement. Par exemple, de nombreuses personnes utilisent la thématisation en appliquant une classe à <html> ou <body>:

<body class="different">
  <x-foo></x-foo>
</body>

Vous pouvez utiliser la fonction :host-context(.different) pour appliquer un style à <x-foo> lorsqu'il s'agit d'un descendant d'un élément ayant la classe .different:

:host-context(.different) {
  color: red;
}

Cela vous permet d'encapsuler des règles de style dans le Shadow DOM d'un élément afin de lui appliquer un style unique en fonction de son contexte.

Assurer la compatibilité avec plusieurs types d'hôtes à partir d'une seule racine fantôme

:host est également utilisé si vous créez une bibliothèque de thématisation et souhaitez appliquer des styles à de nombreux types d'éléments hôtes à partir du même Shadow DOM.

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

Appliquer un style aux composants internes Shadow DOM depuis l'extérieur

Le pseudo-élément ::shadow et le combinateur /deep/ fonctionnent comme si l'utilisateur disposait d'une épée vorpale faisant autorité CSS. Ils permettent de percer les limites de Shadow DOM pour appliquer un style aux éléments situés dans des arbres d'ombre.

Pseudo-élément ::shadow

Si un élément comporte au moins une arborescence fantôme, le pseudo-élément ::shadow correspond à la racine fantôme elle-même. Il vous permet d'écrire des sélecteurs qui appliquent un style aux nœuds internes au domaine d'ombre d'un élément.

Par exemple, si un élément héberge une racine fantôme, vous pouvez écrire #host::shadow span {} pour appliquer un style à tous les étendues de son arborescence d'ombres.

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

Exemple (éléments personnalisés) : <x-tabs> a <x-panel> enfants dans son Shadow DOM. Chaque panneau héberge sa propre arborescence fantôme contenant h2 en-têtes. Pour appliquer un style à ces titres à partir de la page principale, vous pouvez écrire:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

Le combinateur /deep/

Le combinateur /deep/ est semblable à ::shadow, mais il est plus puissant. Elle ignore complètement toutes les limites des ombres et traverse un nombre illimité d'arbres d'ombre. Pour faire simple, /deep/ vous permet d'explorer en détail les intestins d'un élément et de cibler n'importe quel nœud.

Le combinateur /deep/ est particulièrement utile dans le monde des éléments personnalisés, où il est courant d'avoir plusieurs niveaux de Shadow DOM. Les exemples parfaits incluent l'imbrication d'un ensemble d'éléments personnalisés (chacun hébergeant son propre arbre ombré) ou la création d'un élément qui hérite d'un autre à l'aide de <shadow>.

Exemple (éléments personnalisés) : sélectionnez tous les éléments <x-panel> qui sont descendants de <x-tabs>, n'importe où dans l'arborescence :

x-tabs /deep/ x-panel {
    ...
}

Exemple – Appliquez un style à tous les éléments de la classe .library-theme, n'importe où dans une arborescence fantôme:

body /deep/ .library-theme {
    ...
}

Utiliser querySélecteur()

Tout comme .shadowRoot ouvre les arborescences d'ombre pour le balayage DOM, les combinaisons ouvrent ces arborescences pour le balayage des sélecteurs. Au lieu d'écrire une chaîne de folie imbriquée, vous pouvez écrire une seule instruction:

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

Appliquer un style à des éléments natifs

Les commandes HTML natives sont difficiles à styliser. Beaucoup de gens abandonnent et donnent leurs pourboires. Toutefois, avec ::shadow et /deep/, vous pouvez appliquer un style à tout élément de la plate-forme Web qui utilise Shadow DOM. Les types <input> et <video> en sont de bons exemples:

video /deep/ input[type="range"] {
  background: hotpink;
}

Créer des hooks de style

La personnalisation, c'est bien. Dans certains cas, vous pouvez percer des trous dans le bouclier de style de votre Ombre et créer des hooks que d'autres personnes pourront styliser.

Utiliser ::shadow et /deep/

/deep/ offre une grande puissance. Elle permet aux auteurs de composants de désigner des éléments individuels comme personnalisables ou une multitude d'éléments comme personnalisables.

Exemple – Appliquez un style à tous les éléments ayant la classe .library-theme, en ignorant tous les arbres d'ombre:

body /deep/ .library-theme {
    ...
}

Utiliser des pseudo-éléments personnalisés

WebKit et Firefox définissent tous deux des pseudo-éléments permettant de styliser les éléments internes des éléments de navigateur natifs. input[type=range] en est un bon exemple. Vous pouvez styliser la <span style="color:blue">blue</span> du curseur en ciblant ::-webkit-slider-thumb:

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

De la même manière que les navigateurs fournissent des hooks de style dans certains éléments internes, les auteurs du contenu Shadow DOM peuvent désigner certains éléments comme personnalisables par des personnes extérieures. Pour ce faire, utilisez des pseudo-éléments personnalisés.

Vous pouvez désigner un élément en tant que pseudo-élément personnalisé à l'aide de l'attribut pseudo. Sa valeur, ou son nom, doit être précédé de "x-". Cela crée une association avec cet élément dans l'arborescence fantôme et donne aux utilisateurs externes une voie désignée pour traverser la limite de l'ombre.

L'exemple ci-dessous montre comment créer un widget de curseur personnalisé et permettre à un utilisateur de styliser son curseur bleu:

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

Utiliser des variables CSS

Les variables CSS constituent un moyen efficace de créer des hooks de thématisation. Fondamentalement, la création d'« espaces réservés de style » que les autres utilisateurs peuvent remplir.

Imaginez un auteur d'élément personnalisé qui marque des espaces réservés de variables dans son Shadow DOM. L'un pour styliser la police d'un bouton interne et l'autre pour sa couleur:

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

Ensuite, l'intégrateur de l'élément définit ces valeurs à son goût. Pour l'assortir au thème Super Comic Sans de sa page:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

En raison de la manière dont les variables CSS héritent, tout est pêche, et cela fonctionne très bien. La vue d'ensemble ressemble à ceci:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

Réinitialisation des styles

Les styles hérités, comme les polices, les couleurs et les hauteurs de ligne, continuent d'affecter les éléments du Shadow DOM. Cependant, pour une flexibilité maximale, Shadow DOM nous donne la propriété resetStyleInheritance pour contrôler ce qui se passe à la limite de l'ombre. Considérez-la comme un moyen de repartir de zéro lorsque vous créez un composant.

resetStyleInheritance

  • false : valeur par défaut. Les propriétés CSS héritées continuent d'en hériter.
  • true : réinitialise les propriétés pouvant être héritées sur initial au niveau de la limite.

Vous trouverez ci-dessous une démonstration qui montre comment la modification de resetStyleInheritance affecte l'arborescence fantôme:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
Propriétés héritées des outils de développement

Il est plus délicat de comprendre l'élément .resetStyleInheritance, principalement parce qu'il n'affecte que les propriétés CSS qui sont héritées. Il indique que lorsque vous recherchez une propriété à hériter, à la limite entre la page et la ShadowRoot, n'héritez pas des valeurs de l'hôte, mais utilisez plutôt la valeur initial (conformément à la spécification CSS).

Si vous ne savez pas quelles propriétés sont héritées dans CSS, consultez cette liste pratique ou cochez la case "Afficher les propriétés héritées" dans le panneau "Élément".

Appliquer un style aux nœuds distribués

Les nœuds distribués sont des éléments qui s'affichent à un point d'insertion (élément <content>). L'élément <content> vous permet de sélectionner des nœuds à partir du Light DOM et de les afficher à des emplacements prédéfinis dans votre Shadow DOM. Ils ne sont pas logiquement dans le Shadow DOM ; ce sont tout de même des enfants de l'élément hôte. Les points d'insertion sont simplement un élément de rendu.

Les nœuds distribués conservent les styles du document principal. Autrement dit, les règles de style de la page principale continuent de s'appliquer aux éléments, même lorsqu'ils s'affichent au niveau d'un point d'insertion. Là encore, les nœuds distribués sont toujours logiquement dans le domaine léger et ne se déplacent pas. Elles sont simplement affichées ailleurs. Toutefois, lorsque les nœuds sont distribués dans le Shadow DOM, ils peuvent adopter des styles supplémentaires définis dans l'arborescence fantôme.

Pseudo-élément ::content

Les nœuds distribués sont des enfants de l'élément hôte. Comment pouvons-nous les cibler à l'intérieur du Shadow DOM ? La réponse est le pseudo-élément CSS ::content. Il permet de cibler les nœuds Light DOM qui passent par un point d'insertion. Exemple :

::content > h3 applique un style à toutes les balises h3 qui passent par un point d'insertion.

Prenons un exemple:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

Réinitialiser des styles au niveau des points d'insertion

Lorsque vous créez une ShadowRoot, vous avez la possibilité de réinitialiser les styles hérités. <content> et <shadow> points d'insertion proposent également cette option. Lorsque vous utilisez ces éléments, définissez .resetStyleInheritance en JavaScript ou utilisez l'attribut booléen reset-style-inheritance sur l'élément lui-même.

  • Pour des points d'insertion ShadowRoot ou <shadow>: reset-style-inheritance signifie que les propriétés CSS héritées sont définies sur initial au niveau de l'hôte avant d'atteindre votre contenu fictif. C'est ce qu'on appelle la limite supérieure.

  • Pour les points d'insertion <content>: reset-style-inheritance signifie que les propriétés CSS héritables sont définies sur initial avant que les enfants de l'hôte ne soient distribués au niveau du point d'insertion. C'est ce qu'on appelle la limite inférieure.

Conclusion

En tant qu'auteurs d'éléments personnalisés, nous disposons de nombreuses options pour contrôler l'apparence de nos contenus. L'ombre du DOM est la base de ce nouveau monde courageux.

Le Shadow DOM nous offre une encapsulation de style délimitée et permet de laisser entrer autant (ou peu) du monde extérieur que nous le souhaitons. En définissant des pseudo-éléments personnalisés ou en incluant des espaces réservés de variables CSS, les auteurs peuvent fournir à des tiers des hooks de style pratiques pour personnaliser davantage leur contenu. Dans l'ensemble, les auteurs Web ont un contrôle total de la façon dont leur contenu est représenté.