Web Components mit Claude Code entwickeln
Web Components mit Claude Code entwickeln: Custom Elements, Shadow DOM, React/Vue-Integration, Review-Prompts und Tests.
Warum Web Components zu Claude Code passen
Web Components sind Browser-Standards für wiederverwendbare HTML-Elemente. Custom Elements registrieren eigene Tags, Shadow DOM kapselt internes Markup und CSS, und Templates oder Slots helfen bei wiederholbarer Struktur. Der Zweck ist nicht, React oder Vue zu ersetzen. Der Zweck ist ein kleiner, stabiler UI-Vertrag, der in mehreren Umgebungen funktioniert.
Claude Code ist dafür nützlich, weil Web Components vor allem an unklaren Grenzen scheitern. Welche Attribute sind öffentlich? Welche JavaScript-Property darf gesetzt werden? Wie heißt das Event? Darf es aus dem Shadow DOM heraus? Welche CSS-Hooks sind erlaubt? Welche Accessibility-Regeln und Tests schützen das Verhalten? Wenn diese Fragen im Prompt stehen, kann Claude Code implementieren, prüfen und Tests ergänzen, statt nur Demo-Code zu erzeugen.
Masa hatte dieses Problem bei einem React-basierten Mengenfeld, das später in einem CMS und in einer Vue-Oberfläche gebraucht wurde. Für ein kleines Widget React im CMS zu laden war zu teuer, Kopieren führte zu abweichendem Verhalten. Als Web Component konnte dieselbe Balise in statischem HTML, CMS, React und Vue laufen. Die schwierigen Stellen waren nicht die Registrierung des Tags, sondern Shadow-DOM-Styling, Event-Design, Accessibility und Versionierung.
Dieser Artikel baut quantity-stepper: minimale Custom-Element-Implementierung, Attribute-Observation, CustomEvent, Styling-Hooks, Nutzung aus React und Vue, Claude-Code-Review-Prompt und Vitest-Tests. Für die Einordnung in ein Team-Design-System ist der Claude Code Design System Guide die passende Ergänzung.
Offizielle Grundlagen stehen bei MDN Web Components, MDN Using custom elements und MDN CustomEvent.
Geeignete Einsatzfälle
Web Components sind stark, wenn ein kleines, abgeschlossenes UI in mehreren Stacks funktionieren soll. Sie sind weniger passend für ganze Seiten mit Routing, globalem State und komplexem SSR. Beschreibe Claude Code daher zuerst die Zielumgebungen und die Grenzen der Verantwortung.
| Einsatzfall | Warum es passt | Vorgaben für Claude Code |
|---|---|---|
| Design-System-Verteilung | React, Vue und statische Seiten nutzen dasselbe Paket | Tokens, Größen, Zustände, Accessibility |
| CMS-Einbettung | Redakteure erhalten Interaktion ohne App-Runtime | kein globales CSS, sichere Script-Reihenfolge |
| Gemeinsame UI bei Migration | Alte und neue Frontends nutzen dasselbe Element | Attribute, Events, Breaking-Change-Regeln |
| Formularbausteine | Menge, Rating, Autocomplete und Hilfen sind wiederverwendbar | Label, Fehler, Tastatur, Formularbezug |
| Interne Widgets | Admin-Portale und BI-Seiten teilen kleine Tools | keine Secrets in Attributen, Logs, Version |
Gute Kandidaten sind Copy-Buttons, Mengenfelder, Ratings, kleine Suchfelder und Hilfe-Launcher. Ein kompletter Checkout oder ein komplexer Editor sollte eher im Hauptframework bleiben. Wenn ein Element zu viele Verantwortlichkeiten bekommt, wird auch der Claude-Code-Diff unklar.
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"]
Minimales Custom Element
Speichere den folgenden Code als quantity-stepper.ts. Das Element liest HTML-Attribute, normalisiert Zahlen, spiegelt den Wert zurück und sendet bei Benutzerinteraktion 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>
Attribute, Properties und Events
HTML-Attribute sind Strings. JavaScript-Properties können Zahlen, Booleans oder Objekte sein. value="2" und element.value = 2 sind deshalb unterschiedliche Eingänge. Das Element muss festlegen, wie beide synchronisiert werden.
Dieses Beispiel beobachtet value, step und label. Ein Attributwechsel synchronisiert internen State und rendert neu. Ein Klick läuft über den value-Setter, aktualisiert das Attribut, sendet das Event und rendert erneut.
const stepper = document.querySelector("quantity-stepper");
stepper?.addEventListener("quantity-change", (event) => {
const { value } = (event as CustomEvent<{ value: number }>).detail;
console.log("Selected quantity:", value);
});
Das Event ist öffentlicher Vertrag. quantity-change ist klarer als change, { value: number } ist ein kleines stabiles Payload, und composed: true erlaubt das Empfangen außerhalb des Shadow DOM.
Shadow-DOM-Styling
Shadow DOM schützt vor globalem CSS. Das ist in CMS-Seiten und alten Portalen nützlich, aber ohne Theme-Hooks kann das Design-System Farben und Abstände nicht steuern.
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;
}
Gib Claude Code beide Regeln: keine Abhängigkeit von globalem CSS, aber dokumentierte CSS Custom Properties und part-Namen. Prüfe außerdem sichtbaren Fokus, native Buttons, Labels und aria-live. Der Claude Code Accessibility Guide passt als Review-Checkliste.
Nutzung aus React und Vue
React rendert das Custom Element, aber CustomEvent-Handling ist über ein ref am zuverlässigsten.
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 nutzt ein Template-Ref und entfernt den Listener beim Unmount.
<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",
},
},
}),
],
});
Claude-Code-Review-Prompt
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.
So prüft Claude Code nicht nur die Optik, sondern den öffentlichen Vertrag. Eine kurze Version gehört in CLAUDE.md, wenn mehrere Teammitglieder Komponenten ergänzen.
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");
});
});
Diese Tests schützen Attribute, Property, Event-Payload, Shadow-DOM-Propagation und Rendering. Für Produktion ergänze Playwright-Tests für Tastatur, sichtbaren Fokus und echte React/Vue-Seiten.
Typische Fallstricke
Erstens: Shadow DOM und CSS. Ohne Theme-Hooks ist das Element starr, ohne Kapselung kann Host-CSS es zerstören.
Zweitens: Attribute/Property-Synchronisierung. Teste Attributänderung, Property-Setzen und internen Klick als getrennte Pfade.
Drittens: Event-Design. Name, detail, bubbles und composed sind öffentliche API.
Viertens: Accessibility. Ein Custom Tag hat keine automatische Semantik. Nutze native Buttons, Labels, Fokus und Tastaturbedienung.
Fünftens: SSR und Hydration. Vor customElements.define ist das Element noch nicht aktiv. Kritische Flows brauchen Fallbacks.
Sechstens: Versionierung. Attribute, CSS-Variablen, part und Event-Payload müssen dokumentiert und versioniert werden.
Fazit und Prüfnotiz
Web Components sind sinnvoll, wenn kleine UI-Verträge über Framework-Grenzen hinweg stabil bleiben sollen. Claude Code hilft am meisten, wenn der Prompt API, CSS-Grenzen, Events, Accessibility und Tests vorgibt. Lit kann später helfen, ersetzt aber diese Vertragsarbeit nicht.
ClaudeCodeLab unterstützt Teams bei Design-Systemen, CMS-Einbettung, internen Widgets und Claude-Code-Review-Prozessen. Für Arbeit an einem echten Repository ist die Training- und Beratungsseite der passende Einstieg.
Prüfnotiz: Das Beispiel validiert denselben quantity-stepper über Attribute, Property, Shadow-DOM-Buttons, quantity-change und React/Vue-Listener. Vor Produktion sollten Playwright-Tastaturtests und eine Fokusprüfung im echten Browser folgen.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.