Use Cases (Mis à jour: 02/06/2026)

Créer des Web Components avec Claude Code

Créez des Web Components avec Claude Code : Custom Elements, Shadow DOM, React/Vue, prompts de revue et tests.

Créer des Web Components avec Claude Code

Pourquoi associer Claude Code et Web Components

Les Web Components sont des APIs natives du navigateur pour créer des éléments HTML réutilisables. Custom Elements enregistre une balise personnalisée, Shadow DOM isole la structure et les styles internes, et les templates ou slots aident à composer l’interface. Le but n’est pas de remplacer React ou Vue, mais de publier un petit contrat d’interface utilisable dans plusieurs environnements.

Claude Code est pertinent ici parce qu’un Web Component échoue rarement à cause de la syntaxe seule. Il échoue quand son contrat est flou : quels attributs sont publics, quelle property JavaScript est supportée, quel événement sort du Shadow DOM, quels styles peuvent être personnalisés, quelles règles d’accessibilité doivent être respectées. Si ces limites sont écrites dans le prompt, Claude Code peut implémenter, relire et tester avec plus de rigueur.

Masa a rencontré ce problème en voulant réutiliser un petit sélecteur de quantité React dans un CMS et dans un écran Vue. Charger tout React pour ce widget était disproportionné, et recopier le code créait des différences. Le passage en Web Component a permis d’utiliser la même balise dans une page statique, un CMS, React et Vue. Les vraies difficultés ont été la synchronisation attribut/property, les styles Shadow DOM, l’événement public et la stratégie de version.

Dans ce guide, nous construisons quantity-stepper avec une implémentation minimale, l’observation d’attributs, un CustomEvent, des hooks de style, l’usage depuis React et Vue, un prompt de revue Claude Code et des tests Vitest. Pour l’intégrer à une bibliothèque d’équipe, lisez aussi le guide design system Claude Code.

Les références officielles à garder sous la main sont MDN Web Components, MDN Using custom elements et MDN CustomEvent.

Cas d’usage adaptés

Les Web Components conviennent aux composants petits, isolés et partagés. Ils sont moins adaptés aux écrans complets avec routing, état global et rendu serveur complexe. Avant de laisser Claude Code coder, décrivez les environnements cibles et les responsabilités exclues.

Cas d’usagePourquoi c’est utileCe qu’il faut préciser
Design system distribuéReact, Vue et pages statiques consomment le même paquettokens, tailles, états, règles a11y
Intégration CMSUn éditeur ajoute une interaction sans application complètepas de CSS global, chargement script fiable
UI commune multi-frameworkUtile pendant une migration progressiveattributs, événements, versionnement
Composants de formulaireQuantité, rating, autocomplétion, aides de saisielabel, erreur, clavier, formulaire
Widgets internesPortails et dashboards réutilisent la même piècepas de secrets, logs, version du paquet

Les boutons de copie, sélecteurs de quantité, ratings, petits champs de recherche et launchers d’aide sont de bons candidats. Une page de paiement complète ou un éditeur riche très complexe doit plutôt rester dans le framework principal.

flowchart LR
  A["Design tokens"] --> B["Web Component package"]
  B --> C["React app"]
  B --> D["Vue app"]
  B --> E["CMS page"]
  B --> F["Static HTML"]
  B --> G["Internal widget"]

Implémentation minimale

Créez quantity-stepper.ts. Le composant lit des attributs HTML, convertit les valeurs en nombres, reflète la nouvelle valeur et émet quantity-change.

const toNumber = (value: string | null, fallback: number) => {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
};

class QuantityStepper extends HTMLElement {
  static observedAttributes = ["value", "step", "label"];

  #root = this.attachShadow({ mode: "open" });
  #value = 0;
  #step = 1;

  connectedCallback() {
    this.#syncFromAttributes();
    this.#render();
  }

  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
    if (oldValue === newValue || !this.isConnected) return;
    this.#syncFromAttributes();
    this.#render();
  }

  get value() {
    return this.#value;
  }

  set value(nextValue: number) {
    const normalized = Number.isFinite(nextValue) ? nextValue : 0;
    if (normalized === this.#value) return;

    this.#value = normalized;
    this.setAttribute("value", String(normalized));
    this.#emitChange();
    this.#render();
  }

  #syncFromAttributes() {
    this.#value = toNumber(this.getAttribute("value"), 0);
    this.#step = toNumber(this.getAttribute("step"), 1);
  }

  #update(direction: -1 | 1) {
    this.value = this.#value + this.#step * direction;
  }

  #emitChange() {
    this.dispatchEvent(
      new CustomEvent("quantity-change", {
        detail: { value: this.#value },
        bubbles: true,
        composed: true,
      }),
    );
  }

  #render() {
    const label = this.getAttribute("label") || "Quantity";

    this.#root.innerHTML = `
      <style>
        :host {
          --quantity-accent: #2563eb;
          --quantity-border: #cbd5e1;
          --quantity-bg: #ffffff;
          --quantity-text: #0f172a;
          display: inline-flex;
          font-family: system-ui, sans-serif;
        }
        .control {
          display: inline-grid;
          grid-template-columns: 2.5rem minmax(3rem, auto) 2.5rem;
          align-items: center;
          border: 1px solid var(--quantity-border);
          border-radius: 8px;
          background: var(--quantity-bg);
          color: var(--quantity-text);
          overflow: hidden;
        }
        button {
          min-width: 2.5rem;
          min-height: 2.5rem;
          border: 0;
          background: transparent;
          color: var(--quantity-accent);
          font: inherit;
          cursor: pointer;
        }
        button:focus-visible {
          outline: 2px solid var(--quantity-accent);
          outline-offset: -2px;
        }
        output {
          min-width: 3rem;
          text-align: center;
          font-weight: 700;
        }
        .sr-only {
          position: absolute;
          width: 1px;
          height: 1px;
          padding: 0;
          margin: -1px;
          overflow: hidden;
          clip: rect(0, 0, 0, 0);
          white-space: nowrap;
          border: 0;
        }
      </style>

      <div class="control" role="group" aria-label="${label}">
        <button part="button decrement" data-action="decrement" type="button">
          <span aria-hidden="true">-</span>
          <span class="sr-only">Decrease ${label}</span>
        </button>
        <output part="value" aria-live="polite">${this.#value}</output>
        <button part="button increment" data-action="increment" type="button">
          <span aria-hidden="true">+</span>
          <span class="sr-only">Increase ${label}</span>
        </button>
      </div>
    `;

    this.#root
      .querySelector('[data-action="decrement"]')
      ?.addEventListener("click", () => this.#update(-1));

    this.#root
      .querySelector('[data-action="increment"]')
      ?.addEventListener("click", () => this.#update(1));
  }
}

if (!customElements.get("quantity-stepper")) {
  customElements.define("quantity-stepper", QuantityStepper);
}
<script type="module" src="/src/quantity-stepper.ts"></script>

<quantity-stepper value="2" step="1" label="Seats"></quantity-stepper>

Attributs, properties et CustomEvent

Un attribut HTML est une chaîne. Une property JavaScript peut être un nombre. value="2" et element.value = 2 ne passent donc pas par le même chemin. Le composant doit dire quelle source est synchronisée et quel événement prévient le consommateur.

Ici, value, step et label sont observés. Un changement d’attribut déclenche une synchronisation et un rendu. Un clic passe par le setter value, met à jour l’attribut, émet l’événement et rend à nouveau.

const stepper = document.querySelector("quantity-stepper");

stepper?.addEventListener("quantity-change", (event) => {
  const { value } = (event as CustomEvent<{ value: number }>).detail;
  console.log("Selected quantity:", value);
});

Le nom quantity-change est plus clair que change. Le payload { value: number } est petit et stable. bubbles: true et composed: true permettent à React, Vue ou au CMS de recevoir l’événement hors du Shadow DOM.

Styles Shadow DOM

Shadow DOM protège le composant contre les CSS globales, mais il faut ouvrir des points de personnalisation. Sinon le composant ne suivra pas le design system.

quantity-stepper {
  --quantity-accent: #16a34a;
  --quantity-border: #94a3b8;
  --quantity-bg: #f8fafc;
}

quantity-stepper::part(button) {
  font-weight: 700;
}

quantity-stepper::part(value) {
  min-width: 4rem;
}

Demandez à Claude Code de ne pas dépendre du CSS global, tout en exposant des Custom Properties et des part documentés. Vérifiez aussi focus visible, boutons natifs, label et aria-live. Le guide accessibilité Claude Code complète cette revue.

Utilisation depuis React et Vue

React peut afficher la balise, mais le plus fiable pour le CustomEvent est un ref.

import { useEffect, useRef, useState } from "react";
import type { DetailedHTMLProps, HTMLAttributes } from "react";
import "./quantity-stepper";

type QuantityStepperElement = HTMLElement & { value: number };

declare global {
  namespace JSX {
    interface IntrinsicElements {
      "quantity-stepper": DetailedHTMLProps<
        HTMLAttributes<QuantityStepperElement>,
        QuantityStepperElement
      > & {
        value?: string;
        step?: string;
        label?: string;
      };
    }
  }
}

export function QuantityField() {
  const [quantity, setQuantity] = useState(2);
  const ref = useRef<QuantityStepperElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const handleQuantityChange = (event: Event) => {
      setQuantity((event as CustomEvent<{ value: number }>).detail.value);
    };

    element.addEventListener("quantity-change", handleQuantityChange);
    return () => {
      element.removeEventListener("quantity-change", handleQuantityChange);
    };
  }, []);

  return (
    <div>
      <quantity-stepper ref={ref} value="2" step="1" label="Seats" />
      <p>Selected: {quantity}</p>
    </div>
  );
}

Vue utilise une référence de template et nettoie l’écouteur au démontage.

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import "./quantity-stepper";

const quantity = ref(2);
const stepper = ref<HTMLElement | null>(null);

const handleQuantityChange = (event: Event) => {
  quantity.value = (event as CustomEvent<{ value: number }>).detail.value;
};

onMounted(() => {
  stepper.value?.addEventListener("quantity-change", handleQuantityChange);
});

onBeforeUnmount(() => {
  stepper.value?.removeEventListener("quantity-change", handleQuantityChange);
});
</script>

<template>
  <quantity-stepper ref="stepper" value="2" step="1" label="Seats" />
  <p>Selected: {{ quantity }}</p>
</template>
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === "quantity-stepper",
        },
      },
    }),
  ],
});

Prompt de revue Claude Code

You are editing only src/components/quantity-stepper.ts and its tests.

Goal:
Build a framework-agnostic Web Component named quantity-stepper.

Public API:
- attributes: value, step, label
- property: value as number
- event: quantity-change with detail { value: number }
- event must bubble and cross Shadow DOM with composed: true

Implementation rules:
- Use Custom Elements and Shadow DOM.
- Do not depend on React or Vue.
- Expose theme hooks with CSS Custom Properties.
- Expose ::part(button) and ::part(value).
- Keep keyboard and screen reader behavior usable.
- Do not remove visible focus styles.

Review before finishing:
- Attribute/property synchronization
- Event naming and detail shape
- Shadow DOM styling boundaries
- Accessibility labels and aria-live
- Test coverage for click, attribute update, and event emission
Review this Web Component as production code.
Prioritize bugs over style preferences.
Report issues in this order:
1. Broken public API or breaking change risk
2. Accessibility defects
3. Shadow DOM styling leaks
4. React/Vue integration problems
5. Missing tests

For every finding, include the file, line, why it fails, and a minimal fix.

Ce format force une revue critique : API publique, accessibilité, styles, intégration et tests. Une version courte dans CLAUDE.md stabilise le travail d’équipe.

Tests

import { beforeEach, describe, expect, it } from "vitest";
import "./quantity-stepper";

describe("quantity-stepper", () => {
  beforeEach(() => {
    document.body.innerHTML = "";
  });

  it("increments by step and emits a composed event", () => {
    const element = document.createElement("quantity-stepper") as HTMLElement & {
      value: number;
    };

    element.setAttribute("value", "3");
    element.setAttribute("step", "2");
    element.setAttribute("label", "Seats");
    document.body.append(element);

    const events: Array<CustomEvent<{ value: number }>> = [];
    element.addEventListener("quantity-change", (event) => {
      events.push(event as CustomEvent<{ value: number }>);
    });

    element.shadowRoot
      ?.querySelector<HTMLButtonElement>('[data-action="increment"]')
      ?.click();

    expect(element.getAttribute("value")).toBe("5");
    expect(element.value).toBe(5);
    expect(events).toHaveLength(1);
    expect(events[0].detail.value).toBe(5);
    expect(events[0].bubbles).toBe(true);
    expect(events[0].composed).toBe(true);
  });

  it("updates rendering when the value attribute changes", () => {
    const element = document.createElement("quantity-stepper");
    document.body.append(element);

    element.setAttribute("value", "9");

    expect(element.shadowRoot?.querySelector("output")?.textContent).toBe("9");
  });
});

Ces tests protègent le contrat public : attribut, property, événement, propagation et rendu. En production, ajoutez Playwright pour le clavier, le focus visible et l’usage dans une vraie page React ou Vue.

Pièges fréquents

Premier piège : Shadow DOM et CSS. Sans tokens, le composant est impossible à thématiser. Sans isolation, le CSS du site hôte peut le casser.

Deuxième piège : synchronisation attribut/property. Testez le changement HTML, le changement JavaScript et le clic interne.

Troisième piège : événement public. Le nom, detail, bubbles et composed sont des décisions de version.

Quatrième piège : accessibilité. Une balise personnalisée n’a pas de sémantique magique. Utilisez des boutons natifs, des labels, le focus visible et le clavier.

Cinquième piège : SSR et hydration. Avant customElements.define, la balise n’est pas encore active. Les parcours critiques doivent prévoir un fallback.

Sixième piège : versionnement. Attributs, CSS variables, part et événements doivent être documentés et versionnés.

Résumé et note de validation

Les Web Components sont utiles pour publier de petits contrats d’interface à travers plusieurs stacks. Claude Code aide surtout quand le prompt fixe l’API publique, les limites CSS, les événements, l’accessibilité et les tests. Lit peut être ajouté plus tard, mais il ne remplace pas cette discipline.

ClaudeCodeLab peut accompagner la création d’un design system, l’intégration CMS, les widgets internes et le workflow de revue Claude Code. Pour travailler sur un vrai dépôt, consultez la page formation et conseil.

Note de validation : l’exemple vérifie le même contrat quantity-stepper par attributs, property, boutons Shadow DOM, événement quantity-change et consommation React/Vue. Avant production, ajoutez des tests clavier Playwright et une vérification du focus en navigateur réel.

#Claude Code #Web Components #Custom Elements #Shadow DOM #Lit
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.