Zum Inhalt springen

Ein Design-System aufbauen: Von Tokens zu Komponenten

Design-Tokens, Komponenten-APIs und die organisatorischen Muster, die ein Design-System zu einem Multiplikator statt zu einem Wartungsrisiko machen.

4 Min. Lesezeit
Komponentenbibliothek eines Design-Systems mit Tokens, Varianten und Kompositionsmustern

Die meisten Design-Systeme beginnen als Komponentenbibliothek und enden als Engpass. Teams kopieren Komponenten in ihre eigenen Repos, um schneller voranzukommen, die Bibliothek gerät aus dem Takt, und am Ende hast du drei leicht unterschiedliche Buttons in vier Produkten. Das Problem ist meist nicht technisch — es ist architektonisch. Wer das Fundament von Anfang an richtig legt, verhindert diese Drift.

Mit Tokens anfangen, nicht mit Komponenten

Design-Tokens sind die atomare Ebene: benannte Werte für Farbe, Abstände, Typografie und Radius, die an einem einzigen Ort leben und von dort nach außen fließen. Wer Komponenten baut, bevor Tokens etabliert sind, wird überall Werte hartcodieren und ewig refaktorisieren.

tstypescript
// tokens.ts — the single source of truth
export const tokens = {
  color: {
    brand: {
      50: "#eff6ff",
      500: "#3b82f6",
      600: "#2563eb",
      900: "#1e3a8a",
    },
    semantic: {
      primary: "var(--color-brand-500)",
      danger: "var(--color-red-500)",
      success: "var(--color-green-500)",
      warning: "var(--color-yellow-500)",
    },
  },
  spacing: {
    1: "0.25rem",
    2: "0.5rem",
    4: "1rem",
    8: "2rem",
    16: "4rem",
  },
  radius: {
    sm: "0.25rem",
    md: "0.375rem",
    lg: "0.5rem",
    full: "9999px",
  },
} as const;

Generiere CSS Custom Properties aus diesen Tokens als Teil deines Builds. Jede Komponente referenziert var(--color-brand-500), nicht #3b82f6. Theming wird zum Variablentausch statt zu grep-and-replace.

Komponenten-APIs entwerfen, die gut altern

Der größte Wartungskostenfaktor in einem Design-System ist die ständige Änderung der Komponenten-APIs. Hektisch hinzugefügte Props werden permanent — du kannst sie nie wieder entfernen, ohne Konsumenten zu brechen. Entwirf APIs so, als könntest du sie nie wieder ändern.

tsxtsx
// ❌ Prop explosion — every feature request adds a prop
interface ButtonProps {
  label: string;
  isLoading: boolean;
  isDisabled: boolean;
  isFullWidth: boolean;
  iconLeft?: React.ReactNode;
  iconRight?: React.ReactNode;
  loadingText?: string;
  onClick: () => void;
}
 
// ✅ Composition-first — consumers control content, system controls style
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "ghost" | "danger";
  size?: "sm" | "md" | "lg";
  loading?: boolean;
  asChild?: boolean; // Radix UI pattern for polymorphic rendering
}
 
function Button({
  variant = "primary",
  size = "md",
  loading,
  asChild,
  children,
  ...props
}: ButtonProps) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      className={buttonVariants({ variant, size })}
      disabled={loading || props.disabled}
      aria-busy={loading}
      {...props}
    >
      {loading && <Spinner aria-hidden />}
      {children}
    </Comp>
  );
}

Das asChild-Muster (von Radix UI entlehnt) lässt Konsumenten die Button-Styles auf jedem Element rendern — einem Link, einem Router-Link, einem div — ohne eine separate ButtonLink-Variante zu benötigen.

Varianten-Verwaltung mit Class Variance Authority

Varianten-Styles als bedingte Klassen-Strings zu pflegen, wird schnell unüberschaubar. cva (Class Variance Authority) verwandelt Varianten in ein strukturiertes, typsicheres System.

tstypescript
import { cva, type VariantProps } from "class-variance-authority";
 
export const buttonVariants = cva(
  // Base styles — always applied
  "inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        primary:
          "bg-brand-500 text-white hover:bg-brand-600 active:bg-brand-700",
        secondary: "border border-brand-500 text-brand-500 hover:bg-brand-50",
        ghost: "text-brand-500 hover:bg-brand-50",
        danger: "bg-red-500 text-white hover:bg-red-600",
      },
      size: {
        sm: "h-8 px-3 text-sm rounded-md",
        md: "h-10 px-4 text-sm rounded-lg",
        lg: "h-12 px-6 text-base rounded-lg",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  },
);
 
// Type-safe — TypeScript knows which variants exist
type ButtonVariants = VariantProps<typeof buttonVariants>;

cva gibt eine Funktion zurück, die einen Klassen-String liefert. Varianten sind aufzählbar, dokumentiert und typgeprüft. Eine Variante hinzuzufügen ist ein Eintrag in der Config statt einer verstreuten Bedingung.

Kompositionsmuster für Komponenten

Ein Design-System ist nicht nur eine Sammlung von Atomen. Der eigentliche Wert liegt in Compound-Komponenten, die komplexe Interaktionsmuster kapseln — Dinge wie Dropdowns, Dialoge und Command-Menüs, die Teams sonst jedes Mal anders implementieren würden.

tsxtsx
// Compound component pattern: shared state, composable structure
const Dialog = {
  Root: DialogRoot,
  Trigger: DialogTrigger,
  Content: DialogContent,
  Title: DialogTitle,
  Close: DialogClose,
};
 
// Usage — flexible structure, consistent behavior
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <Button variant="danger">Delete Account</Button>
      </Dialog.Trigger>
      <Dialog.Content>
        <Dialog.Title>Are you sure?</Dialog.Title>
        <p>This action is permanent and cannot be undone.</p>
        <div className="flex gap-2 justify-end mt-4">
          <Dialog.Close asChild>
            <Button variant="ghost">Cancel</Button>
          </Dialog.Close>
          <Button variant="danger" onClick={onConfirm}>
            Delete
          </Button>
        </div>
      </Dialog.Content>
    </Dialog.Root>
  );
}

Das Compound-Component-Muster hält Implementierungsdetails (Fokus-Management, ARIA-Attribute, Animation) im System, während Struktur und Inhalt bei den Konsumenten bleiben.

Dokumentation als First-Class Citizen

Eine Komponente, die nicht dokumentiert ist, existiert nicht. Teams werden sie neu implementieren. Jede Komponente braucht drei Dinge in ihrer Dokumentation: wann man sie verwendet, wann nicht, und interaktive Beispiele für jede relevante Variante.

Storybook mit automatisch aus TypeScript-Typen generierten Docs ist der aktuelle Standard. Die entscheidende Konfiguration ist das autodocs-Tag und die argTypes-Inferenz:

tstypescript
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
 
const meta: Meta<typeof Button> = {
  component: Button,
  tags: ["autodocs"], // generates a docs page automatically
  argTypes: {
    variant: { control: "select" },
    size: { control: "radio" },
    loading: { control: "boolean" },
  },
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = { args: { children: "Get started" } };
export const Loading: Story = {
  args: { children: "Saving...", loading: true },
};
export const Danger: Story = {
  args: { children: "Delete", variant: "danger" },
};

Storybook wird zur Vertragsdokumentation. Visual-Regression-Tests (Chromatic, Percy) verhindern Style-Regressionen, während die Bibliothek weiterwächst.

Die wichtigsten Erkenntnisse

  1. Design-Tokens etablieren, bevor irgendeine Komponente geschrieben wird — benannte semantische Werte sind es, die Theming und Konsistenz im großen Maßstab möglich machen
  2. Kompositions-Props gegenüber Feature-Props bevorzugen — asChild, children und das Durchreichen von HTML-Attributen überleben die Prop-Flut
  3. cva macht die Varianten-Verwaltung typsicher und auditierbar — Varianten werden zu einer strukturierten Config, nicht zu verstreuten Bedingungen
  4. Compound-Komponenten kapseln Interaktionsmuster — Teams sollten Verhalten konsumieren, nicht Focus Traps und ARIA neu implementieren
  5. Dokumentation ist ein Produkt-Feature — undokumentierte Komponenten werden neu implementiert; interaktive Storybook-Stories sind das Mindestmaß
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX