Pourquoi le Shadow DOM déclaratif change la donne

Le Shadow DOM est souvent présenté comme une fonctionnalité réservée aux Web Components créés en JavaScript. On définit une classe, on appelle attachShadow(), puis on injecte du HTML et du CSS dans une racine isolée. Cette approche fonctionne, mais elle pose une limite importante : le composant n’existe réellement qu’après exécution du JavaScript.

Le Declarative Shadow DOM permet d’écrire une racine Shadow DOM directement en HTML grâce à un élément template muni de l’attribut shadowrootmode. Le navigateur peut alors construire l’arbre encapsulé dès le parsing du document, sans attendre un script d’hydratation.

C’est particulièrement intéressant pour les sites rendus côté serveur, les architectures hybrides, les design systems et les interfaces qui doivent rester robustes même si JavaScript arrive tard, échoue ou est volontairement limité. Cette logique rejoint directement le progressive enhancement : partir d’un document utile, puis enrichir l’expérience sans rendre la page dépendante d’un runtime complexe.

Rappel : ce que fait le Shadow DOM

Le Shadow DOM permet d’attacher à un élément hôte un sous-arbre DOM séparé du DOM principal. Cette séparation apporte trois bénéfices majeurs :

  • une encapsulation du HTML interne ;
  • une isolation partielle des styles ;
  • une API claire entre l’extérieur et l’intérieur du composant.

Dans un composant web classique, on écrirait par exemple :

class UserBadge extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' })

    shadow.innerHTML = `
      <style>
        .badge {
          display: inline-flex;
          gap: 0.5rem;
          align-items: center;
          padding: 0.5rem 0.75rem;
          border-radius: 999px;
          background: #eef2ff;
        }
      </style>
      <span class="badge">
        <slot name="avatar"></slot>
        <slot></slot>
      </span>
    `
  }
}

customElements.define('user-badge', UserBadge)

Cette approche est pertinente pour des composants interactifs avancés, comme expliqué dans l’article sur les Custom Elements. Mais pour des composants essentiellement structurels ou visuels, elle force parfois l’usage de JavaScript alors que le HTML pourrait déjà contenir la structure finale.

La syntaxe du Declarative Shadow DOM

Avec le Declarative Shadow DOM, le même composant peut être sérialisé directement dans le HTML :

<user-badge>
  <template shadowrootmode="open">
    <style>
      .badge {
        display: inline-flex;
        gap: 0.5rem;
        align-items: center;
        padding: 0.5rem 0.75rem;
        border-radius: 999px;
        background: #eef2ff;
      }

      ::slotted([slot='avatar']) {
        width: 2rem;
        height: 2rem;
        border-radius: 50%;
      }
    </style>

    <span class="badge">
      <slot name="avatar"></slot>
      <slot></slot>
    </span>
  </template>

  <img slot="avatar" src="/images/julien.jpg" alt="" />
  <span>Julien Dargelos</span>
</user-badge>

Le point important est l’attribut shadowrootmode="open". Lors du parsing HTML, le navigateur ne garde pas le template comme simple contenu inerte : il l’utilise pour créer une racine Shadow DOM attachée à user-badge.

Le contenu placé après le template reste dans le light DOM de l’élément hôte. Les éléments slot permettent ensuite de projeter ce contenu dans la structure interne du composant.

Un composant rendu côté serveur

L’intérêt devient évident dans une application SSR. Le serveur peut produire un HTML complet, encapsulé et stylé, sans attendre le bundle JavaScript client. Prenons un composant de carte de formation :

<course-card>
  <template shadowrootmode="open">
    <style>
      article {
        display: grid;
        gap: 0.75rem;
        padding: 1rem;
        border: 1px solid #d4d4d8;
        border-radius: 1rem;
        background: white;
      }

      h3 {
        margin: 0;
        font-size: 1.125rem;
      }

      .meta {
        color: #52525b;
        font-size: 0.875rem;
      }

      a {
        justify-self: start;
      }
    </style>

    <article>
      <h3><slot name="title"></slot></h3>
      <p class="meta"><slot name="meta"></slot></p>
      <p><slot></slot></p>
      <a href="/formations/javascript">Voir la formation</a>
    </article>
  </template>

  <span slot="title">Formation JavaScript moderne</span>
  <span slot="meta">Débutant à intermédiaire · 5 jours</span>
  Apprenez les bases solides du langage, du DOM, de fetch et de l’architecture frontend.
</course-card>

Ce HTML peut être envoyé tel quel par un serveur Node.js, un framework Next.js, Nuxt, Astro ou un générateur statique. Le navigateur affiche une carte structurée, avec ses styles encapsulés, avant même qu’un framework ne prenne la main.

Cette approche complète bien les stratégies qui cherchent à réduire le JavaScript envoyé au navigateur, comme les React Server Components, sans imposer React ni un modèle de composants propriétaire.

Hydrater uniquement ce qui doit l’être

Le Declarative Shadow DOM ne remplace pas JavaScript. Il permet surtout de mieux choisir où JavaScript est réellement nécessaire.

Imaginons un composant de panneau repliable. Le serveur peut rendre la structure, les styles et le contenu. Le client ajoute seulement le comportement interactif :

<details-card>
  <template shadowrootmode="open">
    <style>
      section {
        border: 1px solid #d4d4d8;
        border-radius: 0.75rem;
        overflow: hidden;
      }

      button {
        width: 100%;
        padding: 1rem;
        border: 0;
        text-align: left;
        font: inherit;
        background: #f4f4f5;
        cursor: pointer;
      }

      .content {
        padding: 1rem;
      }

      :host(:not([open])) .content {
        display: none;
      }
    </style>

    <section>
      <button type="button" aria-expanded="false">
        <slot name="title"></slot>
      </button>
      <div class="content">
        <slot></slot>
      </div>
    </section>
  </template>

  <span slot="title">Pré-requis</span>
  Connaître les bases de HTML, CSS et JavaScript.
</details-card>

Puis on ajoute une petite couche TypeScript :

class DetailsCard extends HTMLElement {
  connectedCallback() {
    const button = this.shadowRoot?.querySelector('button')
    if (!button) return

    button.addEventListener('click', () => {
      const isOpen = this.hasAttribute('open')
      this.toggleAttribute('open', !isOpen)
      button.setAttribute('aria-expanded', String(!isOpen))
    })
  }
}

customElements.define('details-card', DetailsCard)

On obtient une hydratation ciblée : le HTML et le CSS sont déjà présents, et JavaScript ne sert qu’à gérer l’état interactif. Cette discipline est précieuse pour la performance, notamment quand elle est combinée à des outils de mesure comme PerformanceObserver.

Slots, styles et design system

Les slots sont essentiels pour concevoir des composants réutilisables. Ils permettent de garder une structure interne stable tout en laissant le contenu varier.

<alert-box>
  <template shadowrootmode="open">
    <style>
      :host {
        display: block;
      }

      aside {
        padding: 1rem;
        border-inline-start: 4px solid var(--alert-color, #2563eb);
        background: var(--alert-bg, #eff6ff);
      }

      strong {
        display: block;
        margin-block-end: 0.25rem;
      }
    </style>

    <aside>
      <strong><slot name="title"></slot></strong>
      <slot></slot>
    </aside>
  </template>

  <span slot="title">Conseil pédagogique</span>
  Commencez par rendre le HTML utile avant d’ajouter les comportements avancés.
</alert-box>

Les variables CSS traversent la frontière du Shadow DOM lorsqu’elles sont définies sur l’hôte ou ses ancêtres. C’est un excellent point d’intégration avec des design tokens en CSS et TypeScript.

alert-box[variant='success'] {
  --alert-color: #16a34a;
  --alert-bg: #f0fdf4;
}

alert-box[variant='warning'] {
  --alert-color: #d97706;
  --alert-bg: #fffbeb;
}

Cela évite de dupliquer des classes internes partout, tout en conservant une personnalisation contrôlée depuis l’extérieur.

Accessibilité : ne pas confondre encapsulation et isolement utilisateur

Le Shadow DOM isole la structure technique, mais il ne doit pas isoler l’expérience utilisateur. Les noms accessibles, les rôles, les libellés, le focus clavier et les relations ARIA doivent rester cohérents.

Quelques règles pratiques :

  • utiliser des éléments natifs quand ils existent ;
  • éviter de recréer un bouton avec un div ;
  • exposer les états importants avec des attributs sur l’hôte ;
  • tester au clavier ;
  • vérifier le rendu avec un lecteur d’écran lorsque le composant est complexe.

Pour un formulaire, par exemple, il faut être particulièrement prudent : les relations entre label, champ, aide et erreur ne doivent pas être cassées par une encapsulation mal pensée. Les principes détaillés dans l’article sur les formulaires accessibles restent prioritaires.

Fallback et compatibilité

Le Declarative Shadow DOM est conçu pour améliorer le rendu initial, mais il faut prévoir les environnements qui ne le prennent pas encore correctement en charge, ou les pipelines qui transforment le HTML.

Une stratégie simple consiste à écrire un petit script de fallback qui détecte les templates déclaratifs restés dans le DOM :

function hydrateDeclarativeShadowDomFallback(root: ParentNode = document) {
  const templates = root.querySelectorAll<HTMLTemplateElement>(
    'template[shadowrootmode]'
  )

  for (const template of templates) {
    const host = template.parentElement
    const mode = template.getAttribute('shadowrootmode')

    if (!host || host.shadowRoot || (mode !== 'open' && mode !== 'closed')) {
      continue
    }

    const shadow = host.attachShadow({ mode })
    shadow.append(template.content.cloneNode(true))
    template.remove()
  }
}

hydrateDeclarativeShadowDomFallback()

Ce fallback reste léger, mais il doit être vu comme une sécurité, pas comme le cœur du modèle. L’objectif est de laisser le navigateur traiter nativement la structure quand il le peut.

Pour suivre précisément le support et les détails d’implémentation, la documentation MDN sur le Declarative Shadow DOM est une ressource utile.

Quand utiliser cette approche

Le Declarative Shadow DOM est pertinent pour :

  • des composants de design system rendus côté serveur ;
  • des cartes, alertes, badges, encarts, panneaux et blocs éditoriaux ;
  • des composants qui doivent être stylés dès le premier rendu ;
  • des pages où l’on veut réduire la dépendance au JavaScript client ;
  • des architectures où plusieurs frameworks doivent cohabiter.

Il est moins adapté si le composant est entièrement dynamique, très dépendant d’un état client complexe, ou si son rendu interne est déjà efficacement géré par un framework avec une stratégie claire d’hydratation.

Les erreurs fréquentes

La première erreur consiste à tout mettre dans le Shadow DOM. L’encapsulation est utile, mais elle ne doit pas devenir un mur. Le contenu éditorial, les liens importants et les informations SEO doivent rester compréhensibles dans le document final.

La deuxième erreur consiste à dupliquer des styles dans chaque instance. Pour des composants répétés des centaines de fois, il faut surveiller le poids HTML généré. Dans certains cas, une feuille de style globale bien structurée avec des cascade layers CSS sera plus économique.

La troisième erreur consiste à oublier que le Shadow DOM modifie les habitudes de test. Les sélecteurs CSS, les tests end-to-end et les outils d’inspection doivent parfois accéder explicitement à shadowRoot.

Conclusion

Le Declarative Shadow DOM rend le modèle des Web Components plus compatible avec le web moderne rendu côté serveur. Il permet de livrer des composants encapsulés, stylés et structurés dès le HTML initial, tout en réservant JavaScript aux comportements réellement nécessaires.

Ce n’est pas une solution magique ni un remplacement des frameworks. C’est plutôt une brique d’architecture : utile pour construire des interfaces plus résilientes, plus sobres en JavaScript et plus faciles à intégrer dans des environnements hétérogènes. Pour un site de formation, une documentation technique ou un design system, c’est une piste solide pour concilier composants réutilisables, performance et progressive enhancement.