Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
12 mars 2026

Scoped registries : comment éviter les collisions de Web Components dans un design system

Quand deux libs veulent enregistrer le même custom element, tout part en vrille. Les scoped registries apportent enfin une isolation propre.

7 min de lecture
12 vues
réactions
Partager :
Scoped registries : comment éviter les collisions de Web Components dans un design system

Les Web Components ont un défaut structurel qui coûte cher en prod : le registre des custom elements est global. Une seule app, une seule page, et soudain tu intègres un widget tiers ou un micro-frontend… et tu te retrouves avec un DOMException parce que <my-button> a déjà été défini. Les scoped registries attaquent ce problème à la racine : enfermer les définitions de composants dans un scope, au lieu de polluer toute la page.

Le problème réel : collisions, versions multiples, micro-frontends

Dans un design system moderne, les collisions n’arrivent pas “par hasard”. Elles arrivent parce que l’architecture y conduit :

  • Deux équipes livrent deux versions d’un même composant (ex. design system v1 dans un legacy, v2 dans un nouveau module).
  • Micro-frontends : chaque fragment bundle ses dépendances, donc tes custom elements peuvent être “redéfinis” plusieurs fois.
  • Widgets tiers (chat, paiement, analytics UI) qui embarquent leurs propres web components.
  • Monorepo + bundlers : une déduplication imparfaite et tu charges deux copies d’une lib de composants sans t’en rendre compte.

Et le pire : le crash est souvent brutal. Le navigateur refuse une redéfinition d’un tag déjà enregistré, et tu peux te retrouver avec un écran blanc pour un détail de chargement.

Pourquoi ça casse : le registre global des custom elements

Le modèle historique est simple :

  • Tu fais customElements.define('ds-button', ...).
  • Le navigateur enregistre ce tag dans un registre global associé au document.
  • À partir de là, ce nom est “pris” pour toute la page.

Ça marche bien tant que tu contrôles 100% du code de la page. Dès que tu as des frontières (micro-frontends, plugins, intégrations), tu te bats contre une contrainte de plateforme.

Scoped registries : l’idée (enfin) saine

Une scoped registry, c’est un registre de custom elements dédié, que tu associes à un sous-arbre (typiquement un shadow root). Résultat :

  • Tu peux définir le même tag dans deux scopes différents.
  • Chaque scope résout les tags selon son propre registre.
  • Tu peux faire coexister deux versions d’un design system sur la même page.

Ce n’est pas juste un “hack” d’intégration : c’est une stratégie d’isolation qui colle parfaitement à l’esprit des Web Components (encapsulation), mais appliquée au problème que la Shadow DOM ne résolvait pas : le nommage global.

Un exemple concret : deux versions du même composant, sans collision

Imaginons un shell applicatif qui doit afficher un module legacy (DS v1) et un module récent (DS v2). Les deux utilisent le même tag <ds-button> (oui, ce genre de décision arrive).

Avec une scoped registry, tu peux isoler les définitions par shadow root :

// DS v1
const registryV1 = new CustomElementRegistry();
registryV1.define('ds-button', class DsButtonV1 extends HTMLElement {
  connectedCallback() {
    this.textContent = this.getAttribute('label') ?? 'Button v1';
  }
});

// DS v2
const registryV2 = new CustomElementRegistry();
registryV2.define('ds-button', class DsButtonV2 extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<strong>${this.getAttribute('label') ?? 'Button v2'}</strong>`;
  }
});

// Host 1
const host1 = document.querySelector('#legacy');
const shadow1 = host1.attachShadow({ mode: 'open', customElements: registryV1 });
shadow1.innerHTML = `<ds-button label="Payer"></ds-button>`;

// Host 2
const host2 = document.querySelector('#modern');
const shadow2 = host2.attachShadow({ mode: 'open', customElements: registryV2 });
shadow2.innerHTML = `<ds-button label="Payer"></ds-button>`;

Le point important n’est pas le code d’exemple : c’est l’architecture. Tu déplaces la responsabilité de la définition du niveau page (global) vers le niveau module (local).

Design system : une stratégie d’architecture qui tient en prod

Si tu veux que ça marche à l’échelle (équipes multiples + intégrations), il faut une règle simple : un design system ne doit pas imposer un enregistrement global par défaut. Il doit être capable de fonctionner en scope.

Règle n°1 : le design system expose une API d’enregistrement

Au lieu de “définir automatiquement” au moment de l’import, tu exposes une fonction d’enregistrement :

  • Elle prend un registre (global ou scoped).
  • Elle définit uniquement les composants demandés (ou un preset).
// côté design system
export function registerDesignSystem(registry) {
  registry.define('ds-button', DsButton);
  registry.define('ds-modal', DsModal);
  // ...
}

// côté app
const registry = new CustomElementRegistry();
registerDesignSystem(registry);
const shadow = document.querySelector('#app-root')
  .attachShadow({ mode: 'open', customElements: registry });

Ça évite le pattern “import = side effects” qui explose dès que tu as plusieurs contextes d’exécution.

Règle n°2 : un scope = une frontière d’intégration

Décide explicitement où tu veux des frontières :

  • Micro-frontends : un scope par fragment (recommandé si chaque fragment amène ses dépendances).
  • Widgets tiers : un scope par widget, même si c’est “juste un encart”.
  • Mode legacy : un scope pour encapsuler l’ancien design system sans contaminer le nouveau.

Le bénéfice immédiat : tu peux upgrader un module indépendamment, sans te battre avec l’ordre de chargement des scripts.

Règle n°3 : l’app shell reste responsable de l’assemblage

En pratique, c’est souvent le shell (ou la plateforme) qui doit :

  • créer les registries,
  • monter les shadow roots,
  • charger les bundles (dynamiquement si besoin),
  • et injecter le HTML/les points de montage.

Les équipes “produit” livrent des composants et des modules, pas une prise de contrôle globale du document.

Micro-frontends : ce que ça change vraiment

Sans scoped registries, la recommandation classique en micro-frontends est : “préfixez vos tags” (ex. mfeA-button, mfeB-button). Ça marche, mais c’est :

  • moche dans le DOM,
  • dur à standardiser dans un design system,
  • et surtout ça ne résout pas la coexistence de versions si le préfixe est le même.

Avec des scopes, tu peux garder un HTML lisible et stable, et déplacer la complexité au bon endroit : l’intégration.

Compatibilité et déploiement : ne fais pas comme si tout le monde l’avait

Point d’attention : comme toute nouveauté plateforme, le support peut être partiel selon les navigateurs et les versions. La bonne approche est progressive enhancement :

  • Feature detection : vérifier que CustomElementRegistry existe et que attachShadow accepte customElements.
  • Fallback : en l’absence de scoped registries, revenir à une stratégie “safe” (préfixe, dédup stricte, ou chargement unique).

Si tu es sur un environnement contrôlé (Chrome/Edge en entreprise, kiosk, apps embarquées), tu peux être plus agressif. Si tu fais du B2C web grand public, tu dois prévoir un plan B.

Check-list d’intégration (celle qui évite les nuits blanches)

  • Interdire les side effects : pas de customElements.define au top-level d’un module importé “au hasard”.
  • Centraliser l’enregistrement : une fonction registerX(registry) par package.
  • Définir des scopes explicites : shell, micro-frontends, widgets tiers, legacy.
  • Observer les collisions en dev : log des définitions, audit des bundles en double.
  • Documenter la règle dans le design system : “où et comment enregistrer”.

Conclusion : l’encapsulation qui manquait aux Web Components

Les scoped registries ne sont pas un gadget. C’est une réponse directe à un problème structurel : le global state du registre. Dans un design system sérieux (et encore plus avec des micro-frontends), ça te donne enfin une isolation propre, sans sacrifier la lisibilité du DOM ni t’imposer des conventions de nommage moches.

Si tu veux des Web Components “industrie”, tu veux cette brique dans ta boîte à outils. Et tu veux surtout adapter ton design system pour qu’il puisse vivre sans monopoliser la page.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !