Shadow DOM 201

CSS et stylisation

Cet article vous explique tout ce que vous pouvez faire avec Shadow DOM. Il s'appuie sur les concepts abordés dans Présentation du Shadow DOM. Pour en savoir plus, consultez cet article.

Introduction

Soyons réalistes. Le balisage sans style n'a rien de sexy. Heureusement pour nous, les génies derrière les composants Web l'ont prévu et ne nous ont pas laissés tomber. Le module de champ d'application CSS définit de nombreuses options de stylisation du contenu dans un arbre d'ombre.

Encapsulation de style

L'une des principales caractéristiques du Shadow DOM est la limite de l'ombre. Il possède de nombreuses propriétés intéressantes, mais l'une des meilleures est qu'il fournit une encapsulation de style sans frais. Autrement dit :

<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>

Cette démonstration fait l'objet de deux observations intéressantes :

  • Il existe d'autres éléments h3 sur cette page, mais le seul qui correspond au sélecteur h3 et qui est donc stylisé en rouge est celui du ShadowRoot. Encore une fois, les styles avec portée par défaut.
  • Les autres règles de style définies sur cette page qui ciblent les éléments h3 ne s'appliquent pas à mon contenu. En effet, les sélecteurs ne traversent pas la limite de l'ombre.

Moral de l'histoire ? Nous avons une encapsulation de style par rapport au monde extérieur. Merci Shadow DOM !

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

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

<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>

Un point à noter est que les règles de la page parente sont plus spécifiques que les règles :host définies dans l'élément, mais moins spécifiques qu'un attribut style défini sur l'élément hôte. Cela permet aux utilisateurs de remplacer votre style de 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 : ne correspondre que 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 utilisateur

: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 elle 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 effectuent la thématisation en appliquant une classe à <html> ou <body> :

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

Vous pouvez :host-context(.different) pour styliser <x-foo> lorsqu'il est descendant d'un élément de la classe .different :

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

Vous pouvez ainsi encapsuler les règles de style dans le Shadow DOM d'un élément pour lui appliquer un style unique, en fonction de son contexte.

Prendre en charge plusieurs types d'hôtes à partir d'une racine fantôme

:host peut également être utilisé si vous créez une bibliothèque de thématisation et que vous souhaitez prendre en charge le style 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 éléments internes du Shadow DOM depuis l'extérieur

Le pseudo-élément ::shadow et le combinateur /deep/ sont comme avoir une épée Vorpale d'autorité CSS. Ils permettent de percer la limite du Shadow DOM pour styliser les éléments dans les arborescences fantômes.

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 fantôme d'un élément.

Par exemple, si un élément héberge une racine d'ombre, vous pouvez écrire #host::shadow span {} pour styliser tous les délais de son arbre d'ombre.

<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 son propre arbre d'ombre contenant des titres h2. Pour styliser 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 plus puissant. Il ignore complètement toutes les limites d'ombre et traverse un nombre illimité d'arbres d'ombre. En termes simples, /deep/ vous permet d'examiner en détail 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 principaux consistent à imbriquer un ensemble d'éléments personnalisés (chacun hébergeant sa propre arborescence "shadow"), ou à créer 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 des descendants de <x-tabs>, n'importe où dans l'arborescence :

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

Exemple : stylisez tous les éléments de la classe .library-theme, n'importe où dans un arbre d'ombre :

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

Utiliser querySelector()

Tout comme .shadowRoot ouvre les arborescences d'ombres pour le balayage de DOM, les combinateurs ouvrent les arborescences d'ombres pour le balayage de sélecteur. Au lieu d'écrire une chaîne imbriquée de folie, 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');

Modifier le style des éléments natifs

Les commandes HTML natives sont difficiles à styliser. Beaucoup de gens abandonnent et font leur propre. Toutefois, avec ::shadow et /deep/, vous pouvez styliser n'importe quel é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 est bonne. Dans certains cas, vous pouvez créer des trous dans le bouclier de stylisation de votre ombre et créer des crochets pour que d'autres puissent le styliser.

Utiliser ::shadow et /deep/

Il y a beaucoup de puissance derrière /deep/. Il permet aux auteurs de composants de désigner des éléments individuels comme stylables ou un grand nombre d'éléments comme thémables.

Exemple : appliquez un style à tous les éléments qui possèdent la classe .library-theme, en ignorant toutes les arborescences fantômes :

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

Utiliser des pseudo-éléments personnalisés

WebKit et Firefox définissent des pseudo-éléments pour styliser les éléments internes des éléments de navigateur natif. Un bon exemple est input[type=range]. Vous pouvez styliser le curseur du curseur <span style="color:blue">blue</span> 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 composants internes, les auteurs du contenu Shadow DOM peuvent désigner certains éléments comme pouvant être stylisés par des utilisateurs externes. Pour ce faire, utilisez des pseudo-éléments personnalisés.

Vous pouvez désigner un élément comme pseudo-élément personnalisé à l'aide de l'attribut pseudo. Sa valeur, ou son nom, doit être précédée du préfixe "x-". Cela crée une association avec cet élément dans l'arborescence d'ombre et permet aux personnes extérieures de traverser la limite d'ombre.

Voici un exemple de création d'un widget de curseur personnalisé permettant à un utilisateur d'appliquer un style à son curseur en 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 sont un moyen efficace de créer des crochets de thématisation. En gros, vous créez des "espaces réservés de style" que les autres utilisateurs peuvent remplir.

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

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

L'intégrateur de l'élément définit ensuite ces valeurs à sa guise. Peut-être pour correspondre au thème Comic Sans super cool de sa propre page :

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

En raison de la façon dont les variables CSS héritent, tout est parfait et cela fonctionne parfaitement. L'image complète se présente comme suit :

<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éritables tels que les polices, les couleurs et les hauteurs de ligne continuent d'affecter les éléments du Shadow DOM. Toutefois, pour une flexibilité maximale, le Shadow DOM nous fournit la propriété resetStyleInheritance pour contrôler ce qui se passe à la limite de l'ombre. Il s'agit d'un moyen de repartir à zéro lorsque vous créez un composant.

resetStyleInheritance

  • false : par défaut. Les propriétés CSS héritables continuent d'en hériter.
  • true : réinitialise les propriétés héritables sur initial à la limite.

Vous trouverez ci-dessous une démonstration montrant l'impact de la modification de resetStyleInheritance sur l'arborescence des ombres :

<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 de DevTools

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

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

Mettre en forme des nœuds distribués

Les nœuds distribués sont des éléments qui s'affichent au niveau d'un point d'insertion (élément <content>). L'élément <content> vous permet de sélectionner des nœuds dans le DOM léger et de les afficher à des emplacements prédéfinis dans votre DOM ombragé. Ils ne se trouvent pas logiquement dans le DOM ombragé. Ils restent des enfants de l'élément hôte. Les points d'insertion ne servent qu'à effectuer un 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 à un point d'insertion. Encore une fois, les nœuds distribués se trouvent toujours logiquement dans le DOM léger et ne bougent pas. Ils s'affichent 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 depuis le Shadow DOM ? La réponse est le pseudo-élément CSS ::content. Il s'agit d'un moyen de cibler les nœuds Light DOM qui passent par un point d'insertion. Exemple :

::content > h3 stylise tous les tags 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. Les points d'insertion <content> et <shadow> disposent également de 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 les points d'insertion ShadowRoot ou <shadow> : reset-style-inheritance signifie que les propriétés CSS héritables sont définies sur initial chez l'hôte, avant qu'elles n'atteignent votre contenu d'ombre. Cet emplacement est appelé "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 point d'insertion. Cet emplacement est appelé "limite inférieure".

Conclusion

En tant qu'auteurs d'éléments personnalisés, nous avons de nombreuses options pour contrôler l'apparence de nos contenus. Le Shadow DOM constitue la base de ce nouvel univers.

Shadow DOM nous permet d'encapsuler des styles délimités et de laisser entrer autant (ou aussi peu) que le 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 hooks de style pratiques à des tiers pour personnaliser davantage leur contenu. Dans l'ensemble, les auteurs Web contrôlent totalement la représentation de leur contenu.