</>
tw
Tailwind CSS · Komponenten · Design System · CVA
Tailwind Komponenten-Bibliothek aufbauen
Wiederverwendbare UI ohne CSS-Chaos

Tailwind CSS verleitet zu duplizierten Klassen-Strings in jedem Template. Eine sauber strukturierte Tailwind Komponenten-Bibliothek mit @layer components, CVA für Varianten und Design-Tokens als Single Source of Truth löst dieses Problem nachhaltig – ohne auf Utility-First-Vorteile zu verzichten.

15 Min. Lesezeit @layer components · CVA · Design-Tokens · Storybook · Varianten Tailwind CSS v3 · v4 · React · Vue · Alpine.js

1. Warum eine Tailwind Komponenten-Bibliothek

Der Utility-First-Ansatz von Tailwind CSS ist produktiv für einzelne Komponenten, skaliert aber ohne Struktur nicht gut in große Teams und langlebige Projekte. Wenn ein Button mit 14 Klassen-Strings in 40 Templates dupliziert ist und man die Hover-Farbe ändert, muss man 40 Stellen editieren. Eine Tailwind Komponenten-Bibliothek löst dieses Problem durch Abstraktion: Die Klassen-Strings werden an einer zentralen Stelle definiert, und alle Verwender referenzieren diese Abstraktion. Das kann eine CSS-Klasse in @layer components, eine JavaScript-Funktion in CVA oder eine Hyvä-Block-Komponente sein.

Der entscheidende Unterschied zu klassischem BEM oder SCSS-Modulen: Eine gut strukturierte Tailwind Komponenten-Bibliothek behält alle Vorteile des Utility-First-Ansatzes. Der JIT-Compiler findet die Klassen weiterhin, weil sie als vollständige statische Strings in den Template-Dateien oder Konfigurationsdateien stehen. Das Theme bleibt die einzige Quelle für Design-Tokens. Und Varianten wie size="lg" oder variant="outline" werden typsicher durch CVA verwaltet, nicht durch manuelle Klassen-Konkatenation in jedem Template.

2. @layer components: Wiederverwendbarkeit ohne Spezifitätskriege

Der @layer components-Block in Tailwind CSS ist der erste Baustein einer Tailwind Komponenten-Bibliothek. Hier definiert man wiederverwendbare Klassen, die intern auf Tailwind-Utilities basieren und dieselbe Spezifität wie Utilities haben. Das ist der entscheidende Unterschied zu gewöhnlichem CSS: Klassen in @layer components können von Utilities überschrieben werden, weil Utilities in einem späteren Layer definiert sind. Das ermöglicht das Pattern "Basisklasse mit Override": class="btn btn-primary py-3"py-3 überschreibt das im Komponenten-Layer definierte Padding.

Wichtig für die Architektur der Tailwind Komponenten-Bibliothek: @layer components sollte sparsam eingesetzt werden. Gute Kandidaten sind Klassen, die in mehr als 5–10 Stellen identisch auftreten und sich konzeptuell zu einer Einheit zusammenfassen lassen – Buttons, Cards, Badges, Formulare. Schlechte Kandidaten sind Klassen für einmalige Layout-Elemente, die besser als Tailwind-Utilities direkt im Template bleiben. Zu viele Komponenten-Layer-Klassen führen zurück zum Problem von SCSS-Architekturen: Abstraktion für ihrer selbst willen, ohne Wartbarkeitsvorteil.


/* styles/components.css — @layer components for reusable UI */
@layer components {

  /* Base button — all variants extend this */
  .btn {
    @apply inline-flex items-center justify-center gap-2 font-semibold rounded-lg
           px-4 py-2 text-sm transition-all duration-150 focus-visible:outline-none
           focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50
           disabled:pointer-events-none;
  }

  /* Variant: primary — main CTA */
  .btn-primary {
    @apply bg-sky-600 text-white hover:bg-sky-700 focus-visible:ring-sky-500;
  }

  /* Variant: outline — secondary action */
  .btn-outline {
    @apply border border-slate-300 bg-white text-slate-700
           hover:bg-slate-50 focus-visible:ring-slate-400;
  }

  /* Variant: ghost — low-emphasis action */
  .btn-ghost {
    @apply text-slate-600 hover:bg-slate-100 focus-visible:ring-slate-400;
  }

  /* Card container */
  .card {
    @apply bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden;
  }

  /* Badge — semantic color variants follow */
  .badge {
    @apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full
           text-xs font-semibold;
  }
  .badge-success { @apply bg-green-100 text-green-700; }
  .badge-warning { @apply bg-yellow-100 text-yellow-700; }
  .badge-error   { @apply bg-red-100 text-red-700; }
}

3. Design-Tokens: Theme als Single Source of Truth

Design-Tokens sind benannte Werte für Farben, Abstände, Typografie und Radii – die kleinsten Einheiten eines Design Systems. In einer Tailwind Komponenten-Bibliothek werden Design-Tokens im Theme-Objekt der tailwind.config.js (v3) oder in der @theme-Direktive der CSS-Datei (v4) definiert. Von dort sind sie automatisch als Tailwind-Klassen nutzbar und stehen gleichzeitig als CSS Custom Properties zur Verfügung – keine doppelte Pflege in SCSS-Variablen und Tailwind-Config.

Die Benennung der Design-Tokens ist entscheidend für die Wartbarkeit der Tailwind Komponenten-Bibliothek. Semantische Namen wie color-surface-primary statt color-white, color-interactive-default statt color-blue-500 entkoppeln die Implementierung vom Erscheinungsbild. Wenn die Markenfarbe von Blau zu Grün wechselt, muss nur der Token-Wert angepasst werden – alle Komponenten, die bg-interactive-default verwenden, erhalten automatisch die neue Farbe. Das ist der Kern einer wartbaren Design-Token-Strategie.

4. Varianten mit CVA: Class Variance Authority

CVA (Class Variance Authority) ist eine TypeScript-Bibliothek, die das Varianten-Management in einer Tailwind Komponenten-Bibliothek systematisiert. Statt Klassen-Strings per Hand zu konkatenieren oder bedingte Logik in Template-Ausdrücken zu schreiben, definiert man Komponenten-Varianten als typsicheres Schema. CVA kombiniert Basis-Klassen, Varianten-Klassen und Compound-Varianten zu einem einzigen Funktionsaufruf, der den korrekten Klassen-String zurückgibt. Das Ergebnis ist vollständige TypeScript-Typsicherheit für alle Komponenten-Props und eine zentrale Quelle für alle Klassen-Definitionen.

Compound-Varianten in CVA sind besonders mächtig für die Tailwind Komponenten-Bibliothek: Sie definieren Klassen, die nur dann aktiv sind, wenn mehrere Varianten gleichzeitig einen bestimmten Wert haben. Ein Button, der sowohl size="xs" als auch variant="icon" ist, bekommt andere Padding-Werte als ein size="xs"-Button mit Text. Dieses Muster wäre mit manueller Klassen-Konkatenation fehleranfällig und schwer zu warten – CVA macht es deklarativ und testbar.


/* components/button.ts — CVA variant definition for Button component */
import { cva, type VariantProps } from 'class-variance-authority'

export const buttonVariants = cva(
  /* Base classes — always applied */
  'inline-flex items-center justify-center gap-2 font-semibold rounded-lg transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
  {
    variants: {
      /* Visual variant */
      variant: {
        primary: 'bg-sky-600 text-white hover:bg-sky-700 focus-visible:ring-sky-500',
        outline: 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-slate-400',
        ghost:   'text-slate-600 hover:bg-slate-100 focus-visible:ring-slate-400',
        danger:  'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500',
      },
      /* Size variant */
      size: {
        xs: 'px-2.5 py-1.5 text-xs',
        sm: 'px-3 py-2 text-sm',
        md: 'px-4 py-2 text-sm',
        lg: 'px-5 py-2.5 text-base',
        xl: 'px-6 py-3 text-lg',
      },
      /* Icon-only variant — replaces text padding with square padding */
      iconOnly: {
        true: '',
      },
    },
    /* Compound: square padding when iconOnly AND a specific size */
    compoundVariants: [
      { iconOnly: true, size: 'xs', class: 'p-1.5' },
      { iconOnly: true, size: 'sm', class: 'p-2' },
      { iconOnly: true, size: 'md', class: 'p-2.5' },
      { iconOnly: true, size: 'lg', class: 'p-3' },
    ],
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)

/* TypeScript type for props — inferred from CVA schema */
export type ButtonVariants = VariantProps<typeof buttonVariants>

5. Button-Komponente als vollständiges Beispiel

Der Button ist die häufigste Komponente in jeder Tailwind Komponenten-Bibliothek und eignet sich ideal als Vorlage für das Varianten-System. Ein vollständiger Button in der Bibliothek besteht aus drei Schichten: dem CVA-Schema für Klassen-Varianten, der Framework-Komponente (React, Vue oder Alpine.js), die das Schema nutzt, und der Storybook-Story für die visuelle Dokumentation. Alle drei Schichten zusammen ergeben eine Komponente, die vollständig typsicher, visuell dokumentiert und ohne Klassen-Duplizierung wiederverwendbar ist.

Ein häufiger Fehler beim Aufbau einer Tailwind Komponenten-Bibliothek: zu viele Varianten zu früh definieren. Jede neue Variante erhöht die Komplexität des Schemas und die Anzahl der Klassen, die der JIT-Compiler finden muss. Stattdessen empfiehlt sich das "YAGNI"-Prinzip: zunächst die minimale Varianten-Menge implementieren, die der aktuelle Designbedarf erfordert, und Varianten nur bei realem Bedarf ergänzen. Die Bibliothek bleibt so schlanker und einfacher zu dokumentieren.

6. Storybook-Integration für die Dokumentation

Storybook ist das Standardwerkzeug für die Dokumentation von Tailwind Komponenten-Bibliotheken. Es rendert Komponenten isoliert, ermöglicht interaktive Varianten-Controls und exportiert eine statische Dokumentationswebseite für das Team. Die Integration mit Tailwind CSS erfordert, dass Storybooks Webpack- oder Vite-Konfiguration dieselbe PostCSS- oder Vite-Plugin-Konfiguration verwendet wie das Hauptprojekt – damit der JIT-Compiler auch die Story-Dateien scannt und alle verwendeten Klassen im CSS enthält.

Ein praktisches Muster für Tailwind Komponenten-Bibliotheken in Storybook: Argtype-Definitionen direkt aus CVA-Schemas ableiten. Da CVA eine typsichere Varianten-Definition liefert, lassen sich die Varianten-Optionen programmatisch in Storybook-Controls umwandeln – ohne sie doppelt zu pflegen. Eine Button-Story mit allen CVA-Varianten als Controls lässt sich mit wenigen Zeilen generieren und zeigt dem Team automatisch, welche Varianten verfügbar sind und wie sie aussehen.


/* Button.stories.ts — Storybook story with CVA-derived controls */
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  /* argTypes derived from CVA schema — no duplication */
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'outline', 'ghost', 'danger'],
    },
    size: {
      control: 'select',
      options: ['xs', 'sm', 'md', 'lg', 'xl'],
    },
    iconOnly: { control: 'boolean' },
    disabled: { control: 'boolean' },
    children: { control: 'text' },
  },
  /* Default story args */
  args: {
    children: 'Button',
    variant: 'primary',
    size: 'md',
  },
}
export default meta

type Story = StoryObj<typeof Button>

/* All variants in one story for visual regression testing */
export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-wrap gap-3 p-6 bg-slate-50 rounded-xl">
      {['primary', 'outline', 'ghost', 'danger'].map((variant) => (
        <Button key={variant} variant={variant as any}>
          {variant}
        </Button>
      ))}
    </div>
  ),
}

7. Komponenten für Alpine.js ohne Framework-Overhead

Alpine.js-Projekte wie Magento Hyvä profitieren von einer Tailwind Komponenten-Bibliothek auf andere Weise als React- oder Vue-Projekte: Es gibt keine Komponenten-Dateien im Framework-Sinne, sondern Phtml-Templates mit Alpine.js-Direktiven. Die Bibliothek besteht hier aus zwei Teilen: CSS-Klassen in @layer components für die visuellen Bausteine und JavaScript-Objekte in Alpine.store oder Alpine.data für Verhaltensmuster, die über mehrere Templates genutzt werden. Diese Trennung hält Tailwind-Klassen und Alpine.js-Logik wartbar getrennt.

Ein Beispiel aus der Praxis: Ein Dropdown-Menü in einer Tailwind Komponenten-Bibliothek für Hyvä definiert in @layer components die Klassen .dropdown-trigger, .dropdown-menu und .dropdown-item. Gleichzeitig wird in Alpine.data('dropdown', ...) das Öffnen/Schließen-Verhalten definiert. Das Phtml-Template nutzt beides: class="dropdown-trigger" für das Styling und x-data="dropdown()" für das Verhalten. Diese Architektur ermöglicht, das Styling zu ändern, ohne den Alpine.js-Code anzufassen – und umgekehrt.

8. @apply vs. CVA vs. HTML-Klassen: Direkter Vergleich

Die Frage, welche Abstraktion für eine Tailwind Komponenten-Bibliothek die richtige ist, hängt vom Projektkontext ab. Alle drei Ansätze haben Stärken und Schwächen.

Ansatz Vorteile Nachteile Empfehlung
HTML-Klassen duplizieren Kein Overhead, JIT findet alle Klassen sicher Wartungsaufwand bei Änderungen, Inkonsistenz Nur für einmalige Elemente
@apply in CSS Klasse im Template kurz, CSS-native Lösung @apply gilt als Anti-Pattern in v4, Purge-Risiko Für PHP/Phtml ohne Framework
CVA (JavaScript) Typsicher, Varianten deklarativ, dokumentierbar Nur in JS/TS-Projekten nutzbar, Build-Schritt React, Vue, Svelte-Projekte
Template-Komponenten Wiederverwendung auf Template-Ebene Framework-spezifisch (PHP, Blade, Twig) Magento, Laravel, Symfony

In der Praxis ist die beste Tailwind Komponenten-Bibliothek eine Kombination: CSS-Klassen in @layer components für visuelle Bausteine ohne Framework-Abhängigkeit, CVA für komplexe Varianten in JavaScript-Projekten, und Template-Komponenten für Framework-spezifische Wiederverwendung. Der Schlüssel ist, nicht alle drei gleichzeitig für dieselbe Komponente zu nutzen – das erzeugt Abstraktion ohne Mehrwert.

9. Bibliothek skalieren: Namensräume und Versionierung

Wenn eine Tailwind Komponenten-Bibliothek wächst, wird Namensraum-Management wichtig. Klassennamen wie .btn oder .card können mit anderen CSS-Bibliotheken kollidieren, die im Projekt eingebunden sind. Ein Präfix-Namensraum wie .ui-btn, .ui-card verhindert Kollisionen und macht sofort sichtbar, welche Klassen aus der eigenen Bibliothek stammen. In Tailwind v4 lässt sich der Präfix in der CSS-Konfiguration zentral definieren, sodass alle Utilities und Components automatisch präfixiert werden.

Versionierung ist bei einer Tailwind Komponenten-Bibliothek, die als NPM-Paket veröffentlicht wird, über Semantic Versioning (SemVer) geregelt. Breaking Changes in Varianten-APIs oder Token-Namen erfordern einen Major-Version-Bump. Additive Änderungen wie neue Varianten oder Token sind Minor-Releases. Bugfixes an bestehenden Klassen sind Patch-Releases. Dieses Prinzip gilt auch für interne Bibliotheken – ein CHANGELOG und klare Migrationshinweise ersparen dem Team erheblichen Aufwand bei Updates.

10. Zusammenfassung

Eine skalierbare Tailwind Komponenten-Bibliothek kombiniert @layer components für CSS-native Wiederverwendung, CVA für typsicheres Varianten-Management in JavaScript-Projekten und Design-Tokens im Tailwind-Theme als Single Source of Truth. Sie behält alle Vorteile des Utility-First-Ansatzes: JIT-Compiler findet alle Klassen, Overrides mit Utilities sind jederzeit möglich, und die Bundle-Größe bleibt minimal. Storybook dokumentiert die Bibliothek visuell und macht sie für das Team zugänglich.

Die wichtigste Architektur-Entscheidung: Tailwind Komponenten-Bibliothek-Abstraktionen nur dort einführen, wo echte Wiederverwendung stattfindet. Zu frühe Abstraktion erzeugt Komplexität ohne Wartbarkeitsvorteil. Beginne mit direkten Tailwind-Klassen im Template, extrahiere in @layer components oder CVA, wenn eine Komponente an 5+ Stellen identisch auftaucht – dann ist die Extraktion gerechtfertigt und die Bibliothek bleibt schlank.

Tailwind Komponenten-Bibliothek — Das Wichtigste auf einen Blick

@layer components

CSS-native Wiederverwendung mit Utility-First-Kompatibilität. Utilities können @layer-Klassen überschreiben. Nur für echte Wiederverwendungsfälle nutzen.

CVA für Varianten

Typsicheres Varianten-Schema für React/Vue/Svelte. Compound-Varianten für Kombinationen. Argtype-Ableitung für Storybook ohne Duplizierung.

Design-Tokens

Tailwind-Theme als Single Source of Truth. Semantische Namen entkoppeln Implementierung vom Erscheinungsbild. CSS Custom Properties automatisch verfügbar.

Skalierung

Präfix-Namensräume verhindern Kollisionen. SemVer für externe Pakete. CHANGELOG und Migrationshinweise bei Breaking Changes.