Shadow DOM 201

CSS et stylisation

Cet article présente d'autres fonctionnalités étonnantes que vous pouvez utiliser 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.

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

Thématiser un élément

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

:host-context() est souvent 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 des règles de style dans le Shadow DOM d'un élément qui lui donne un style unique, en fonction de son contexte.

Prise en charge de plusieurs types d'hôtes à partir d'une même 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>. */
}

Mettre en forme les éléments internes de Shadow DOM de l'extérieur

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

Pseudo-élément ::shadow

Si un élément comporte au moins un arbre d'ombre, le pseudo-élément ::shadow correspond à la racine d'ombre elle-même. Il vous permet d'écrire des sélecteurs qui stylisent les nœuds internes au DOM de l'ombre 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 des enfants <x-panel> 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 {
    ...
}

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. Par exemple, vous pouvez imbriquer un ensemble d'éléments personnalisés (chacun hébergeant son propre arbre d'ombre) 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 une arborescence d'ombre :

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

Utiliser querySelector()

Tout comme .shadowRoot ouvre les arbres d'ombre pour la traversée du DOM, les combinateurs ouvrent les arbres d'ombre pour la traversée des sélecteurs. 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. De nombreuses personnes abandonnent et créent leur propre solution. Toutefois, avec ::shadow et /deep/, n'importe quel élément de la plate-forme Web qui utilise Shadow DOM peut être stylisé. 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/

/deep/ est très puissant. 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 : stylisez tous les éléments de 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 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;
}

Comme les navigateurs fournissent des crochets de style dans certains éléments internes, les auteurs de contenu Shadow DOM peuvent désigner certains éléments comme pouvant être stylisés par des personnes extérieures. 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é et d'autorisation à un utilisateur de styliser le 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. 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);
}

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éinitialiser les 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 : valeur par défaut. Les propriétés CSS héritables continuent d'être héritées.
  • 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 sont héritées en CSS, consultez cette liste pratique ou cochez la case "Afficher les héritées" 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 à un point d'insertion (un é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 sont qu'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 à 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 de l'ombre.

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 les styles aux points d'insertion

Lorsque vous créez un ShadowRoot, vous pouvez 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 JS 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.

Le Shadow DOM nous offre une encapsulation de style limitée et un moyen d'intégrer autant (ou aussi 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 aux tiers des crochets de style pratiques pour personnaliser davantage leur contenu. En résumé, les auteurs Web contrôlent entièrement la façon dont leurs contenus sont représentés.