<v/>
{ }
Vue.js · Tailwind CSS · cva · Design Tokens · Komponenten
Vue und Tailwind
Sinnvolle Patterns statt Klassenmüll

Ein Button mit 30 Tailwind-Klassen im Template ist kein Tailwind-Problem – es ist ein Architekturproblem. Vue und Tailwind lassen sich so kombinieren, dass Klassenlogik in computed-Properties lebt, Varianten durch cva verwaltet werden und Templates lesbar bleiben, ohne auf die Expressivität von Tailwind zu verzichten.

14 Min. Lesezeit computed · cva · @apply · CSS-Variablen · Tailwind v4 Vue 3 · Tailwind CSS v4 · TypeScript

1. Warum Vue und Tailwind oft chaotisch werden

Die Kombination aus Vue und Tailwind führt in vielen Projekten zu einem spezifischen Antipattern: immer länger werdende :class-Attribute, die Basis-Klassen, Zustandsklassen und Variantenklassen in einem einzigen String-Ausdruck mischen. Ein Button, der in verschiedenen Größen, Farben und Zuständen existiert, sammelt schnell 25–40 Klassen an. Das Template wird zur primären Stelle für Styling-Logik, was bedeutet, dass ein Designer oder ein anderer Entwickler, der die Klassen-Menge reduzieren oder ändern möchte, tief in die Komponentenlogik eingreifen muss, um zu verstehen, welche Klassen konditionell sind und welche immer aktiv.

Das Problem liegt nicht in Tailwind, sondern in der fehlenden Trennung zwischen Styling-Logik und Template-Struktur. Vue bietet mit computed-Properties, Composables und der Komponentenarchitektur alle Werkzeuge, um Klassenlogik zu kapseln und wiederverwendbar zu machen. Die Lösung ist keine Abkehr von Tailwind – im Gegenteil. Die Utility-First-Philosophie entfaltet ihr Potenzial erst dann vollständig, wenn die Klassenlogik in gekapselten Abstraktionen lebt und das Template nur noch deklarativ beschreibt, welche Variante und welcher Zustand aktiv ist, ohne die konkreten Klassen zu kennen.

2. Computed-Properties für Klassenlogik

Der erste Schritt zu sauberem Vue und Tailwind-Code ist die Verlagerung der Klassenlogik aus dem Template in computed-Properties. Statt :class="[isActive ? 'bg-green-500 text-white' : 'bg-white text-slate-800', isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-green-600', size === 'lg' ? 'px-6 py-3 text-lg' : 'px-4 py-2 text-sm']" im Template steht dort nur noch :class="buttonClasses". Die gesamte Logik liegt in einem computed-Property, das klar benannte Schlüssel für jeden Zustand hat, leicht lesbar ist und in Isolation getestet werden kann.

Das computed-Objekt-Syntax von Vue { 'bg-green-500': isActive, 'opacity-50': isDisabled } kombiniert sich sauber mit String-Arrays. Für komplexe Varianten empfiehlt sich das Aufteilen in mehrere kleine computeds: baseClasses für immer aktive Klassen, variantClasses für Größen- und Farb-Varianten und stateClasses für Hover, Focus und Disabled-Zustände. Das macht die Logik modular und erleichtert das Hinzufügen neuer Varianten ohne das bestehende System zu brechen.


// BaseButton.vue — Computed classes instead of inline class logic
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  disabled: false,
  loading: false,
})

// Base classes: always active
const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'

// Size variant map
const sizeClasses: Record<NonNullable<Props['size']>, string> = {
  sm: 'px-3 py-1.5 text-sm gap-1.5',
  md: 'px-4 py-2 text-sm gap-2',
  lg: 'px-6 py-3 text-base gap-2.5',
}

// Color variant map
const variantClasses: Record<NonNullable<Props['variant']>, string> = {
  primary: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500',
  secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200 focus:ring-slate-400',
  ghost: 'bg-transparent text-slate-700 hover:bg-slate-100 focus:ring-slate-400',
}

// Composed class string via computed
const buttonClasses = computed(() => [
  baseClasses,
  sizeClasses[props.size],
  variantClasses[props.variant],
  { 'opacity-50 cursor-not-allowed pointer-events-none': props.disabled || props.loading },
])
</script>
// Template: <button :class="buttonClasses" :disabled="disabled || loading">

3. class-variance-authority: Varianten typsicher verwalten

Class-variance-authority (cva) ist eine kleine Bibliothek, die das Muster der Varianten-Klassenlogik formalisiert und typsicher macht. Statt manuell erstellter Maps und conditionals definiert man mit cva() Basis-Klassen, Varianten und deren Standardwerte in einer deklarativen Struktur. Das Ergebnis ist eine Funktion, die Props-Objekte entgegennimmt und den zusammengesetzten Klassen-String zurückgibt. Der entscheidende Vorteil: TypeScript inferiert automatisch die erlaubten Werte für jede Variante aus der cva-Definition. Ungültige Werte werden zum Compile-Zeitfehler, nicht zur Laufzeit-Überraschung.

cva unterstützt auch Compound Variants – Klassen, die nur aktiv sind, wenn mehrere Varianten gleichzeitig einen bestimmten Wert haben. Das ermöglicht z. B. eine Klasse, die nur beim primary-Button in der lg-Größe aktiv ist, ohne separate if-Blöcke. Die Kombination von Vue und Tailwind mit cva macht Komponentenbibliotheken mit konsistenten Varianten aufbaubar, die typsicher, dokumentiert und bei Hinzufügen neuer Varianten erweiterbar sind, ohne bestehenden Code zu ändern.

4. Komponenten-Abstraktion: wann @apply sinnvoll ist

Die Tailwind-Direktive @apply ist umstritten, aber ihr Anwendungsfall ist klar: Sie ist dann sinnvoll, wenn eine Menge von Utilities eine semantisch bedeutsame Abstraktion bildet, die in mehreren Templates vorkommt, aber zu klein ist für eine eigene Vue-Komponente. Das Paradebeispiel sind typografische Stile: Ein .prose-heading-Klassen-Set aus text-2xl font-bold leading-tight tracking-tight text-slate-900, das in zehn verschiedenen Templates für <h2>-Überschriften verwendet wird, ist ein guter Kandidat für @apply. Eine Button-Klasse hingegen, die sich je nach Kontext unterscheidet und Props-basierte Varianten hat, gehört in eine Komponente.

Die Faustregel: @apply für statische, nicht-variante Utility-Kombinationen, die in reinen HTML-Elementen (nicht Komponenten) verwendet werden. Vue-Komponenten für alles, was Props, Slots, Events oder Varianten hat. Das verhindert, dass @apply zum Ersatz für CSS-in-CSS wird und die Tailwind-Klassen in verschachtelten @apply-Regeln verschwinden, was das CSS-First-Prinzip von Tailwind v4 unterläuft. Wichtig: @apply mit Tailwind v4 funktioniert nur in CSS-Dateien, nicht in <style>-Blöcken von Vue-SFCs ohne explizite PostCSS-Konfiguration.

5. Tailwind v4: CSS-first und @theme

Tailwind v4 bringt mit dem CSS-first-Ansatz eine grundlegende Veränderung: Die Konfiguration findet nicht mehr in tailwind.config.js statt, sondern direkt in der CSS-Datei mit @import "tailwindcss" und @theme-Blöcken. Das hat für Vue und Tailwind-Projekte mehrere Vorteile: Kein JavaScript-Konfigurations-Boilerplate, direkter CSS-Variablen-Zugriff auf Theme-Werte und vollständige Integration mit dem Design-Token-System. Ein @theme-Block definiert Custom Properties, die Tailwind als Utility-Klassen-Basis nutzt und gleichzeitig als CSS-Variablen im gesamten Dokument bereitstellt.

Für Vue und Tailwind v4-Projekte bedeutet das: Theme-Werte sind nicht nur über theme('colors.green.600') in JavaScript zugänglich, sondern direkt als var(--color-green-600) in CSS und auch in Inline-Styles von Vue-Templates. Die Vite-Integration von Tailwind v4 über @tailwindcss/vite ersetzt den bisherigen PostCSS-Plugin-Ansatz und verarbeitet Tailwind-Klassen direkt im Vite-Build-Prozess – schneller und ohne separate PostCSS-Konfigurationsdatei.


/* main.css — Tailwind v4 CSS-first configuration */
@import "tailwindcss";

/* Custom theme tokens — available as CSS vars AND Tailwind utilities */
@theme {
  --color-brand-50: oklch(97% 0.02 145);
  --color-brand-500: oklch(60% 0.18 145);
  --color-brand-600: oklch(52% 0.20 145);
  --color-brand-700: oklch(44% 0.18 145);

  --font-sans: 'Inter Variable', ui-sans-serif, system-ui;
  --radius-xl: 0.875rem;
  --radius-2xl: 1.25rem;

  --spacing-18: 4.5rem;
  --spacing-22: 5.5rem;
}

/* Static abstractions with @apply — NOT for variant-based components */
@layer components {
  .heading-xl {
    @apply text-4xl font-bold leading-tight tracking-tight text-slate-900;
  }
  .heading-lg {
    @apply text-2xl font-bold leading-snug text-slate-900;
  }
  .body-lg {
    @apply text-lg leading-relaxed text-slate-700;
  }
}

/* vite.config.ts — Tailwind v4 Vite plugin */
// import tailwindcss from '@tailwindcss/vite'
// plugins: [vue(), tailwindcss()]
// No postcss.config.js needed

6. Design Tokens mit CSS-Variablen

Design Tokens sind benannte Designentscheidungen – Farben, Abstände, Schriften, Radii – die als einzige Quelle der Wahrheit dienen und in allen Teilen des Systems (Vue-Komponenten, CSS, Inline-Styles) zugänglich sind. Mit Tailwind v4 und CSS-Variablen konvergieren diese Konzepte: @theme-Werte sind gleichzeitig Tailwind-Utilities und CSS Custom Properties. Eine Farbe, die als --color-brand-600 definiert ist, erzeugt automatisch Utilities wie bg-brand-600, text-brand-600, border-brand-600 und ist via var(--color-brand-600) in jedem CSS-Kontext nutzbar.

In Vue-Komponenten lassen sich Design Tokens elegant für dynamische Inline-Styles nutzen, die nicht durch Tailwind-Utilities abgedeckt sind. :style="{ '--progress': progressPercent + '%' }" setzt eine CSS-Variable, die dann in einer @keyframes-Animation oder einer clip-path-Regel referenziert wird. Das trennt den dynamischen Wert (aus Vue-State) von der Presentationslogik (im CSS), ohne dass ein separater JavaScript-Watcher die DOM-Properties manuell aktualisieren muss. Diese Kombination aus Vue-Reaktivität und CSS-Custom-Properties ist eines der elegantesten Patterns in modernen Vue und Tailwind-Projekten.

7. Dark Mode in Vue mit Tailwind

Dark Mode mit Tailwind in einer Vue-Anwendung zu implementieren erfordert die Entscheidung zwischen dem media-Strategie (folgt dem System-Setting des Nutzers) und der class-Strategie (manuelles Umschalten per CSS-Klasse am html-Element). Für Anwendungen mit einem expliziten Theme-Switcher ist die class-Strategie die richtige Wahl: Ein Composable verwaltet den aktuellen Theme-Zustand in einem Pinia-Store oder localStorage, und ein Watcher fügt die dark-Klasse am document.documentElement hinzu oder entfernt sie.

Tailwind-Klassen mit dark:-Modifier verhalten sich in Vue-Komponenten wie alle anderen Tailwind-Klassen – sie funktionieren in computed-Properties, cva-Definitionen und @apply-Regeln ohne Änderungen. Wichtig: Tailwind v4 unterstützt mit @theme dark das Überschreiben von Design-Token-Werten für den Dark Mode direkt in CSS, ohne die dark:-Modifier auf jedes einzelne Element zu setzen. Das ermöglicht kohärentes Dark-Mode-Theming auf Token-Ebene statt auf Komponenten-Ebene.

8. Responsive Design-Patterns für Komponenten

Responsive Vue-Komponenten mit Tailwind folgen dem Mobile-First-Prinzip: Basis-Klassen ohne Breakpoint-Prefix sind für mobile Geräte, sm:, md:, lg: und xl: überschreiben diese für größere Viewports. In computed-Properties lässt sich dieses Muster direkt anwenden: Tailwind-Responsive-Klassen sind normale Strings und benötigen keine spezielle Behandlung in Vue. Die Herausforderung entsteht, wenn Responsive-Verhalten Props-abhängig ist: Ein Grid-Komponent, der :cols="3" Props nimmt und daraus grid-cols-3 generiert, kann nicht einfach dynamische Klassen zusammensetzen, weil Tailwind nur statisch analysierte Klassen in den Build einschließt.

Die Lösung: Vollständige Klassen-Strings in einer Lookup-Map speichern statt dynamisch zusammenzusetzen. const colsMap = { 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4' } statt `grid-cols-${props.cols}`. Tailwind's statischer Analyzer kann grid-cols-${n} nicht auflösen und schließt diese Klassen nicht in den Build ein. Wer auf dynamische Klassen angewiesen ist, muss sie in der safelist-Konfiguration explizit listen – in Tailwind v4 via @source in der CSS-Datei oder via safelist-Option im Vite-Plugin.

9. Tailwind-Patterns im Vergleich

Die Wahl des richtigen Vue und Tailwind-Patterns hängt vom Komplexitätsgrad der Komponente und der Anzahl der Varianten ab. Folgende Tabelle hilft bei der Entscheidung:

Situation Schlechtes Pattern Empfohlenes Pattern Grund
Button-Varianten 30+ Klassen im Template cva in Komponente Typsicher, erweiterbar
Dynamische Klassen `grid-cols-${n}` Lookup-Map mit vollen Klassen Tailwind-Analyzer erkennt Klassen
Typografie Gleiche 8 Klassen 20x kopiert @apply in Layer components DRY, statisch, keine Varianten
Theme-Farben Hardcoded HEX in Inline-Style CSS-Variablen via @theme Konsistenz, Dark-Mode-fähig
Zustands-Klassen Ternäre Ausdrücke im Template Computed-Property mit Objekt-Syntax Lesbar, testbar, wartbar

Die Hauptregel für Vue und Tailwind: Templates beschreiben Struktur und Zustand, computed-Properties kapseln Klassenlogik, cva verwaltet Varianten, @apply abstrahiert statische Utility-Kombinationen und Design Tokens liegen in @theme-CSS-Variablen. Diese Aufteilung hält jede Schicht fokussiert und vermeidet das Klassenmüll-Antipattern, ohne die Expressivität von Tailwind einzuschränken.

Mironsoft

Vue.js Frontend-Entwicklung, Tailwind CSS und Komponentenbibliotheken

Vue und Tailwind sauber strukturieren?

Wir bauen Vue-Komponentenbibliotheken mit Tailwind und cva – typsicher, wartbar und mit klarer Trennung zwischen Struktur und Styling-Logik.

Komponenten-Audit

Bestehende Komponenten auf Klassenmüll, duplizierte Logik und fehlende Abstraktion analysieren

Design-System

Design-Token-System mit Tailwind v4 und CSS-Variablen – konsistent, Dark-Mode-fähig, erweiterbar

cva-Migration

Bestehende Varianten-Logik auf cva migrieren – mit TypeScript-Typsicherheit und automatischer Prop-Inferenz

10. Zusammenfassung

Sauberes Vue und Tailwind-Code entsteht durch konsequente Trennung der Verantwortlichkeiten: Templates beschreiben Struktur, computed-Properties kapseln Klassenlogik, cva verwaltet Varianten typsicher, @apply abstrahiert statische Utility-Kombinationen ohne Varianten und Design Tokens leben in @theme-CSS-Variablen. Das verhindert das Klassenmüll-Antipattern, ohne die Expressivität und Flexibilität von Tailwind einzuschränken.

Tailwind v4 mit dem CSS-first-Ansatz und @theme-Blöcken macht das Design-Token-System kohärenter: Custom Properties sind gleichzeitig Tailwind-Utilities und CSS-Variablen, die in Inline-Styles von Vue-Komponenten nutzbar sind. Wer diese Patterns konsequent anwendet, baut UI-Komponentenbibliotheken, die bei neuen Varianten erweiterbar sind, bei Refactorings stabil bleiben und für jeden Entwickler im Team lesbar sind – ohne Kenntnis jeder einzelnen Tailwind-Utility.

Vue und Tailwind Patterns — Das Wichtigste auf einen Blick

Klassenlogik kapseln

computed-Properties statt inline :class-Ausdrücke. Basis, Varianten und Zustands-Klassen in separaten, testbaren computeds.

cva für Varianten

class-variance-authority liefert TypeScript-Typsicherheit für Komponentenvarianten – ungültige Prop-Werte werden Compile-Fehler, nicht Laufzeit-Bugs.

@apply für Statisches

Nur für statische, nicht-variante Utility-Kombinationen. Nicht für Komponenten mit Props und Events – die gehören in Vue-Komponenten.

Tailwind v4 @theme

Design Tokens als CSS-Variablen in @theme-Blöcken – gleichzeitig Tailwind-Utilities und var()-Werte in Inline-Styles und CSS.

11. FAQ: Vue und Tailwind – Patterns und Best Practices

1Warum Klassen nicht im Template belassen?
Lange class-Attribute mischen Logik und Struktur, sind schwer lesbar und machen Refactorings teuer. computed-Properties kapseln Klassenlogik testbar und halten Templates fokussiert.
2Was ist cva und wann brauche ich es?
class-variance-authority für typsichere Varianten – wenn Größe × Farbe × Zustand mehrere Dimensionen ergeben. TypeScript inferiert erlaubte Prop-Werte automatisch.
3Wann @apply verwenden?
Nur für statische, nicht-variante Utility-Gruppen in reinen HTML-Elementen – z. B. Typografie-Sets. Nicht für Komponenten mit Props und Varianten.
4Dynamische Tailwind-Klassen in Vue?
Tailwind analysiert statisch – `grid-cols-${n}` wird nicht erkannt. Lookup-Maps mit vollständigen Klassen-Strings verwenden: { 2: 'grid-cols-2', 3: 'grid-cols-3' }.
5Was ist neu in Tailwind v4?
CSS-first mit @import 'tailwindcss' und @theme statt tailwind.config.js. Vite-Plugin ersetzt PostCSS. Tokens sind gleichzeitig CSS-Variablen und Utilities.
6Dark Mode mit Tailwind in Vue?
Class-Strategie: dark-Klasse am html-Element per Vue-Composable setzen. Tailwind v4 ermöglicht @theme dark für Token-Level-Dark-Mode.
7Fehlende Klassen im Build vermeiden?
Vollständige Klassen-Strings statt dynamisch zusammengesetzte. Fehlende Klassen via @source in CSS oder safelist im Vite-Plugin explizit listen.
8cva mit TypeScript in Vue?
Vollständig typisiert. type ButtonProps = VariantProps<typeof buttonVariants> leitet Props-Interfaces direkt ab. Ungültige Werte werden Compile-Fehler.
9CSS-Variablen vs. Tailwind-Tokens?
In Tailwind v4 dasselbe: @theme-Werte erzeugen CSS Custom Properties und Tailwind-Utilities gleichzeitig. Einmal definieren, überall nutzbar.
10Computed-Klassen testen?
computed-Properties sind reine Funktionen – direkt mit Vitest testbar ohne DOM-Rendering: expect(buttonClasses.value).toContain('bg-green-600').