Pourquoi les Constructable Stylesheets changent la manière de styliser les composants

Quand on crée des composants web réutilisables, la question du style revient très vite. Faut-il injecter une balise style dans chaque Shadow DOM ? Importer une feuille CSS globale ? Dupliquer les mêmes règles dans chaque composant ? Toutes ces approches fonctionnent, mais elles montrent leurs limites dès qu’un projet grossit.

Les Constructable Stylesheets apportent une réponse plus structurée : créer une feuille de style CSS en JavaScript, puis l’adopter dans un document ou dans un Shadow DOM via adoptedStyleSheets.

L’idée est simple : au lieu de recopier le même CSS dans chaque instance d’un composant, on construit une seule feuille de style réutilisable. Cela devient particulièrement intéressant dans une bibliothèque de composants, un design system, ou une application qui utilise fortement le Shadow DOM.

Si vous travaillez déjà avec des composants natifs, cet article complète bien les bases présentées dans les Custom Elements, ainsi que l’approche plus globale des Web Components dans un design system.

Le problème des styles dupliqués dans le Shadow DOM

Le Shadow DOM permet d’encapsuler la structure et les styles d’un composant. C’est une excellente propriété pour éviter les collisions CSS, mais elle a un coût : chaque racine Shadow possède son propre périmètre de style.

Une implémentation classique ressemble souvent à ceci :

class UserBadge extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: "open" });

    root.innerHTML = `
      <style>
        .badge {
          display: inline-flex;
          align-items: center;
          gap: 0.5rem;
          padding: 0.375rem 0.75rem;
          border-radius: 999px;
          background: #eef2ff;
          color: #3730a3;
          font: 500 0.875rem system-ui;
        }
      </style>
      <span class="badge">
        <slot></slot>
      </span>
    `;
  }
}

customElements.define("user-badge", UserBadge);

Pour un composant isolé, ce code est acceptable. Mais avec cinquante composants, ou mille instances du même composant, le CSS est répété dans le DOM. Le navigateur peut optimiser certaines choses, mais l’architecture reste moins claire : les styles sont mélangés au template, difficiles à partager, et parfois compliqués à tester.

Les Constructable Stylesheets permettent de sortir ce CSS du rendu du composant.

Créer une feuille CSS réutilisable avec CSSStyleSheet

Une Constructable Stylesheet est une instance de CSSStyleSheet créée directement en JavaScript.

const badgeStyles = new CSSStyleSheet();

badgeStyles.replaceSync(`
  .badge {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.75rem;
    border-radius: 999px;
    background: var(--badge-bg, #eef2ff);
    color: var(--badge-color, #3730a3);
    font: 500 0.875rem system-ui;
  }
`);

La méthode replaceSync() remplace le contenu de la feuille de style de manière synchrone. Il existe aussi replace(), qui retourne une promesse et peut être utile si le contenu est généré ou chargé dynamiquement.

Une fois la feuille créée, on peut l’attacher à un Shadow DOM avec adoptedStyleSheets.

class UserBadge extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: "open" });

    root.adoptedStyleSheets = [badgeStyles];

    root.innerHTML = `
      <span class="badge">
        <slot></slot>
      </span>
    `;
  }
}

customElements.define("user-badge", UserBadge);

Le style est maintenant partagé. Toutes les instances de user-badge peuvent utiliser la même feuille, sans dupliquer le bloc CSS dans chaque Shadow DOM.

Composer plusieurs feuilles de style

Dans une vraie architecture frontend, les styles ne se limitent pas à un composant. On trouve souvent des tokens, des styles de base, des utilitaires internes et des variantes.

Les Constructable Stylesheets sont intéressantes parce qu’elles se composent naturellement avec des tableaux.

export const tokens = new CSSStyleSheet();

tokens.replaceSync(`
  :host {
    --radius-pill: 999px;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --color-accent-bg: #eef2ff;
    --color-accent-text: #3730a3;
  }
`);

export const badgeStyles = new CSSStyleSheet();

badgeStyles.replaceSync(`
  .badge {
    display: inline-flex;
    gap: var(--space-2);
    padding: 0.375rem var(--space-3);
    border-radius: var(--radius-pill);
    background: var(--badge-bg, var(--color-accent-bg));
    color: var(--badge-color, var(--color-accent-text));
  }
`);

Puis dans le composant :

root.adoptedStyleSheets = [tokens, badgeStyles];

Cette approche s’accorde très bien avec une stratégie de design tokens en CSS et TypeScript. Les tokens définissent le vocabulaire visuel, tandis que les feuilles spécifiques décrivent les composants.

Mettre à jour dynamiquement une feuille partagée

Une particularité importante : si plusieurs composants adoptent la même feuille, une modification de cette feuille se répercute partout.

const theme = new CSSStyleSheet();

theme.replaceSync(`
  :host {
    --surface: white;
    --text: #111827;
  }
`);

export function enableDarkTheme() {
  theme.replaceSync(`
    :host {
      --surface: #111827;
      --text: #f9fafb;
    }
  `);
}

Cette capacité peut être utile pour un thème applicatif, mais elle demande de la discipline. Une feuille partagée est un état partagé. Si elle est modifiée depuis n’importe où, le comportement devient difficile à prédire.

Dans la plupart des cas, il est préférable de rendre les feuilles de style quasiment immuables, puis de piloter les variantes avec des attributs, des classes ou des custom properties CSS.

:host([variant="success"]) .badge {
  --badge-bg: #ecfdf5;
  --badge-color: #047857;
}

:host([variant="danger"]) .badge {
  --badge-bg: #fef2f2;
  --badge-color: #b91c1c;
}

Cette séparation garde le CSS déclaratif, lisible et compatible avec les pratiques présentées dans CSS cascade layers, même si les layers et les feuilles adoptées ne répondent pas exactement au même problème.

Utiliser adoptedStyleSheets sur le document

adoptedStyleSheets n’est pas réservé au Shadow DOM. On peut aussi l’utiliser sur document.

const globalUtilities = new CSSStyleSheet();

globalUtilities.replaceSync(`
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
`);

document.adoptedStyleSheets = [
  ...document.adoptedStyleSheets,
  globalUtilities
];

Cela peut servir pour de petits ensembles de styles générés par JavaScript. En revanche, il ne faut pas transformer tout son CSS global en Constructable Stylesheets par principe. Pour les styles principaux d’une page, un fichier CSS classique reste souvent plus simple, plus compatible avec les outils de build, et plus naturel pour le cache HTTP.

Les Constructable Stylesheets sont surtout pertinentes quand le style est fortement lié à des composants JavaScript, notamment avec Shadow DOM.

Intégration avec un composant Vue ou React

Les frameworks comme Vue et React n’ont pas besoin des Constructable Stylesheets pour styliser leurs composants classiques. Leurs écosystèmes disposent déjà de nombreuses approches : CSS Modules, scoped styles, CSS-in-JS, fichiers CSS importés, conventions de design system.

En revanche, si vous exposez des Web Components consommés depuis Vue ou React, les feuilles adoptées deviennent très utiles. Le composant natif garde son encapsulation, quel que soit le framework qui l’utilise.

Exemple côté Vue :

<script setup lang="ts">
const userName = "Ada Lovelace";
</script>

<template>
  <user-badge variant="success">
    {{ userName }}
  </user-badge>
</template>

Exemple côté React :

export function ProfileHeader() {
  return (
    <user-badge variant="success">
      Ada Lovelace
    </user-badge>
  );
}

Le style reste dans le Web Component, sans dépendre du système CSS de l’application hôte. C’est un point clé pour créer des composants réellement portables.

Fallback et progressive enhancement

Comme pour beaucoup d’API web modernes, il faut prévoir le cas où adoptedStyleSheets n’est pas disponible dans l’environnement ciblé, ou dans certains outils de test.

Une stratégie simple consiste à détecter le support et à injecter une balise style en fallback.

function applyStyles(root: ShadowRoot, sheet: CSSStyleSheet, cssText: string) {
  if ("adoptedStyleSheets" in root) {
    root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
    return;
  }

  const style = document.createElement("style");
  style.textContent = cssText;
  root.append(style);
}

Pour éviter de maintenir deux sources différentes, vous pouvez stocker le CSS dans une constante.

const badgeCss = `
  .badge {
    display: inline-flex;
    border-radius: 999px;
  }
`;

const badgeSheet = new CSSStyleSheet();
badgeSheet.replaceSync(badgeCss);

Cette logique rejoint le principe de progressive enhancement : utiliser une API moderne quand elle est disponible, sans rendre le composant inutilisable ailleurs.

Bonnes pratiques à retenir

Les Constructable Stylesheets sont puissantes, mais elles ne doivent pas devenir un prétexte pour déplacer toute l’architecture CSS dans JavaScript.

Utilisez-les lorsque vous avez des composants encapsulés, des styles réellement partagés entre plusieurs Shadow DOM, ou une bibliothèque de composants distribuée. Gardez des fichiers CSS classiques pour les styles globaux, les layouts de page et les règles qui n’ont pas besoin d’être construites dynamiquement.

Évitez aussi de modifier trop souvent une feuille partagée avec replaceSync(). Pour les états d’interface, préférez les attributs, les classes et les variables CSS. Le navigateur sait très bien recalculer des styles à partir de règles déclaratives ; il n’est pas nécessaire de régénérer du CSS à chaque interaction.

Enfin, documentez clairement quelles feuilles sont globales, quelles feuilles sont propres aux composants, et quelles feuilles contiennent les tokens. Cette organisation devient essentielle dans les projets qui combinent Shadow DOM, design tokens et composants distribués.

Pour approfondir la partie plateforme, la documentation MDN sur [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) et [Document.adoptedStyleSheets](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets) constitue une bonne base technique.

Conclusion

Les Constructable Stylesheets ne remplacent pas CSS, ni les fichiers .[css](/articles/css-style-page-web), ni les conventions d’architecture existantes. Elles ajoutent un outil précis à la boîte à outils frontend : partager efficacement des feuilles de style entre documents et Shadow DOM.

Dans une application classique, vous n’en aurez peut-être jamais besoin. Dans un design system fondé sur les Web Components, elles peuvent en revanche améliorer la maintenabilité, réduire la duplication et clarifier la séparation entre structure, comportement et styles.

C’est précisément dans ce type de cas que l’API devient intéressante : non pas parce qu’elle est moderne, mais parce qu’elle résout un vrai problème d’architecture.