Wenn KI Tailwind-Code schreibt: Vom Utility-Salat zu sauberen Variants in Tailwind 4
Wie du KI-generierten Tailwind-Code systematisch verbesserst und automatisch in konsistente Design-Systeme überführst
Das Problem: KI schreibt Tailwind 3-„Utility-Novels”
Wer sich mit ChatGPT & Co. Quellcode generieren lässt, kennt das: Buttons mit 20+ Klassen, harte Farbcodes (bg-blue-500), inkonsistente Abstände und States, die sich kreuz und quer durchs Projekt ziehen.
Beispiel aus der Praxis:
<button
class="bg-blue-500 text-white p-4 rounded-lg shadow-lg hover:bg-blue-600 transition duration-300 ease-in-out flex items-center justify-center gap-2 w-full max-w-sm mx-auto border border-blue-700"
>
Kaufen
</button>
Das sieht auf den ersten Blick modern aus – ist aber in Wahrheit schwer wartbarer Utility-Salat.
Wie wir bereits in unserem Artikel zu Tailwind Best Practices und Version 4 gezeigt haben, führt dieser Ansatz zu erheblichen Wartungsproblemen.
Typische Probleme mit KI-generiertem Tailwind-Code
Tailwind 3-Output: KI greift oft auf alte Beispiele zurück Keine Wiederverwendung: Jeder Button hat eigene Varianten Keine Tokens: Farben, Spacing, Typografie sind hart verdrahtet Responsiveness/States zufällig gestreut: Kein System erkennbar
Das Ergebnis: Wartungshölle.
Die Lösung: Systemdenken mit Tailwind 4
Tailwind 4 bringt die richtigen Werkzeuge mit, um Ordnung reinzubringen – und zwingt fast schon dazu.
Die Grundidee: Tokens, Utilities, Variants.
- Tokens: Zentrale Werte für Farben, Abstände, Typo in
@theme - Utilities: Wiederkehrende Muster per
@utilityextrahieren - Variants: Systematische Varianten (primary, secondary, ghost, Größen etc.)
- Guardrails: CI/Tools sorgen dafür, dass KI-Ausgaben automatisch normalisiert werden
Umsetzung Schritt für Schritt
1. Projekt-Setup in Astro mit Tailwind 4
src/styles/theme.css
@import 'tailwindcss';
@theme {
--color-brand-primary: #1e40af;
--color-brand-secondary: #f59e0b;
--color-neutral-50: #f9fafb;
--color-neutral-900: #111827;
--spacing-xs: 0.5rem;
--spacing-sm: 0.75rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--radius-button: 0.75rem;
--radius-card: 0.5rem;
--transition-fast: 150ms;
--transition-normal: 200ms;
}
Dieses Theme definiert alle wichtigen Design-Tokens als CSS-Variablen.
2. Wiederkehrende Muster mit @utility
src/styles/components.css
@utility btn {
@apply inline-flex items-center justify-center font-medium
rounded-[--radius-button] transition duration-[--transition-normal]
focus:outline-none focus-visible:ring-2 gap-2
disabled:opacity-50 disabled:cursor-not-allowed;
}
@utility card {
@apply bg-white rounded-[--radius-card] shadow-sm border border-neutral-200;
}
@utility input {
@apply block w-full rounded-[--radius-button] border border-neutral-300
px-3 py-2 text-sm placeholder-neutral-500
focus:border-brand-primary focus:ring-1 focus:ring-brand-primary;
}
3. Varianten als Datenattribute
/* Button Varianten */
.btn[data-variant='primary'] {
@apply bg-brand-primary text-white hover:opacity-90
focus-visible:ring-brand-primary;
}
.btn[data-variant='secondary'] {
@apply bg-white text-brand-primary border border-brand-primary
hover:bg-brand-primary/5 focus-visible:ring-brand-primary;
}
.btn[data-variant='ghost'] {
@apply text-brand-primary hover:bg-brand-primary/10
focus-visible:ring-brand-primary;
}
.btn[data-variant='danger'] {
@apply bg-red-600 text-white hover:bg-red-700
focus-visible:ring-red-600;
}
/* Button Größen */
.btn[data-size='sm'] {
@apply h-8 px-3 text-sm;
}
.btn[data-size='md'] {
@apply h-10 px-4 text-base;
}
.btn[data-size='lg'] {
@apply h-12 px-6 text-lg;
}
.btn[data-size='xl'] {
@apply h-14 px-8 text-xl;
}
4. Verwendung in Astro-Komponente
src/components/Button.astro
---
export interface Props {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg' | 'xl';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
class?: string;
}
const {
variant = 'primary',
size = 'md',
disabled = false,
type = 'button',
class: className = '',
...props
} = Astro.props;
const classes = `btn ${className}`.trim();
---
<button
class={classes}
data-variant={variant}
data-size={size}
disabled={disabled}
type={type}
{...props}
>
<slot />
</button>
Nutzung:
<Button variant="primary" size="lg">Kaufen</Button>
<Button variant="secondary" size="sm">Zurück</Button>
<Button variant="ghost" disabled>Nicht verfügbar</Button>
5. Alternative: tailwind-variants (cva)
Für komplexere Projekte bietet sich cva oder tailwind-variants an:
Installation:
npm install cva
src/components/ButtonWithCVA.astro
---
import { cva, type VariantProps } from 'cva';
const button = cva(
// Base styles
'inline-flex items-center justify-center font-medium rounded-[--radius-button] transition duration-[--transition-normal] focus:outline-none focus-visible:ring-2 gap-2 disabled:opacity-50 disabled:cursor-not-allowed',
{
variants: {
variant: {
primary:
'bg-brand-primary text-white hover:opacity-90 focus-visible:ring-brand-primary',
secondary:
'bg-white text-brand-primary border border-brand-primary hover:bg-brand-primary/5 focus-visible:ring-brand-primary',
ghost:
'text-brand-primary hover:bg-brand-primary/10 focus-visible:ring-brand-primary',
danger:
'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-lg',
xl: 'h-14 px-8 text-xl',
},
fullWidth: {
true: 'w-full',
false: 'w-auto',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
fullWidth: false,
},
}
);
export interface Props extends VariantProps<typeof button> {
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
class?: string;
}
const {
variant,
size,
fullWidth,
disabled = false,
type = 'button',
class: className = '',
...props
} = Astro.props;
---
<button
class={button({ variant, size, fullWidth, class: className })}
disabled={disabled}
type={type}
{...props}
>
<slot />
</button>
Erweiterte Nutzung:
<ButtonWithCVA variant="ghost" size="sm" fullWidth>Mehr anzeigen</ButtonWithCVA>
<ButtonWithCVA variant="danger" size="lg">Löschen</ButtonWithCVA>
6. Komplexere Komponenten: Card mit Variants
src/components/Card.astro
---
import { cva } from 'cva';
const card = cva(
'rounded-[--radius-card] border transition-all duration-[--transition-normal]',
{
variants: {
variant: {
default: 'bg-white border-neutral-200 shadow-sm',
elevated: 'bg-white border-neutral-200 shadow-lg',
outlined: 'bg-white border-neutral-300 shadow-none',
ghost: 'bg-transparent border-transparent shadow-none',
},
padding: {
none: 'p-0',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
},
interactive: {
true: 'hover:shadow-md cursor-pointer',
false: '',
},
},
defaultVariants: {
variant: 'default',
padding: 'md',
interactive: false,
},
}
);
const {
variant,
padding,
interactive,
class: className = '',
...props
} = Astro.props;
---
<div
class={card({ variant, padding, interactive, class: className })}
{...props}
>
<slot />
</div>
7. Input-Komponente mit States
src/components/Input.astro
---
import { cva } from 'cva';
const input = cva(
'block w-full rounded-[--radius-button] px-3 py-2 text-sm transition duration-[--transition-fast] placeholder-neutral-500 disabled:bg-neutral-50 disabled:cursor-not-allowed',
{
variants: {
state: {
default:
'border border-neutral-300 focus:border-brand-primary focus:ring-1 focus:ring-brand-primary',
error:
'border border-red-300 focus:border-red-500 focus:ring-1 focus:ring-red-500',
success:
'border border-green-300 focus:border-green-500 focus:ring-1 focus:ring-green-500',
},
size: {
sm: 'h-8 text-xs',
md: 'h-10 text-sm',
lg: 'h-12 text-base',
},
},
defaultVariants: {
state: 'default',
size: 'md',
},
}
);
export interface Props {
state?: 'default' | 'error' | 'success';
size?: 'sm' | 'md' | 'lg';
label?: string;
error?: string;
hint?: string;
required?: boolean;
}
const {
state,
size,
label,
error,
hint,
required,
class: className = '',
...props
} = Astro.props;
const finalState = error ? 'error' : state;
---
<div class="space-y-1">
{
label && (
<label class="block text-sm font-medium text-neutral-700">
{label}
{required && <span class="text-red-500 ml-1">*</span>}
</label>
)
}
<input
class={input({ state: finalState, size, class: className })}
aria-invalid={finalState === 'error' ? 'true' : 'false'}
aria-describedby={error
? `${props.id}-error`
: hint
? `${props.id}-hint`
: undefined}
{...props}
/>
{
error && (
<p id={`${props.id}-error`} class="text-sm text-red-600">
{error}
</p>
)
}
{
hint && !error && (
<p id={`${props.id}-hint`} class="text-sm text-neutral-500">
{hint}
</p>
)
}
</div>
Guardrails gegen KI-Utility-Salat
Selbst mit Konventionen wirst du KI-Output bekommen wie:
<button class="bg-blue-500 text-white p-4 rounded-lg hover:bg-blue-600">
Buy
</button>
Automatische Korrektur durch Tools
1. Prettier Plugin für Klassenreihenfolge:
npm install -D prettier-plugin-tailwindcss
2. Klassen-Roman-Detector Script:
scripts/check-classes.mjs
import fs from 'node:fs';
import path from 'node:path';
import { glob } from 'glob';
const MAX_CLASSES = 9;
const FORBIDDEN_VALUES = [
/bg-\w+-\d+/, // bg-blue-500
/#[0-9a-fA-F]+/, // Hex-Farben
/text-\[\d+px\]/, // Rohe px-Werte
];
async function checkFiles() {
const files = await glob('src/**/*.{astro,html,tsx,jsx}', {
ignore: 'node_modules/**',
});
let errors = 0;
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
const classRegex = /class(?:Name)?\s*=\s*["`]([^"`]+)["`]/g;
let match;
while ((match = classRegex.exec(content))) {
const classes = match[1].split(/\s+/).filter(Boolean);
// Check class count
if (classes.length > MAX_CLASSES) {
console.error(
`❌ ${file}: ${classes.length} Klassen gefunden (max: ${MAX_CLASSES})`
);
console.error(` Extrahiere in @utility oder Component: ${match[1]}`);
errors++;
}
// Check forbidden values
for (const cls of classes) {
for (const forbidden of FORBIDDEN_VALUES) {
if (forbidden.test(cls)) {
console.error(`❌ ${file}: Verbotener Wert "${cls}"`);
console.error(` Nutze Theme-Tokens statt Rohwerte`);
errors++;
}
}
}
}
}
if (errors > 0) {
console.error(`\n🚨 ${errors} Fehler gefunden!`);
process.exit(1);
} else {
console.log('✅ Alle Klassen sind sauber!');
}
}
checkFiles().catch(console.error);
3. ESLint Plugin für Tailwind:
npm install -D eslint-plugin-tailwindcss
eslint.config.js
import tailwind from 'eslint-plugin-tailwindcss';
export default [
{
plugins: {
tailwindcss: tailwind,
},
rules: {
'tailwindcss/classnames-order': 'error',
'tailwindcss/no-custom-classname': 'warn',
'tailwindcss/no-contradicting-classname': 'error',
},
},
];
CI/CD Integration
package.json Scripts:
{
"scripts": {
"lint:classes": "node scripts/check-classes.mjs",
"lint:tailwind": "eslint src --ext .astro,.ts,.tsx",
"format": "prettier --write src && npm run lint:tailwind -- --fix",
"pre-commit": "npm run lint:classes && npm run lint:tailwind"
}
}
GitHub Actions Workflow:
name: Code Quality
on: [push, pull_request]
jobs:
check-tailwind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm run lint:classes
- run: npm run lint:tailwind
Pre-commit Hooks
Installation von husky:
npm install -D husky
npx husky init
.husky/pre-commit
#!/usr/bin/env sh
npm run pre-commit
Prompting-Strategien für besseren KI-Output
System-Prompt für ChatGPT/Claude
Du bist ein Tailwind 4 Experte. Befolge diese Regeln:
1. NIEMALS mehr als 8 Utility-Klassen pro Element
2. IMMER Theme-Tokens verwenden (bg-brand-primary statt bg-blue-500)
3. Wiederholende Patterns in @utility oder cva extrahieren
4. Astro-Komponenten mit TypeScript Props verwenden
5. Variants über data-attributes oder cva definieren
Beispiel für einen Button:
```astro
<Button variant="primary" size="lg">Text</Button>
NICHT:
<button
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
></button>
### Komponenten-Template für Prompts
Erstelle eine Astro-Komponente mit folgender Struktur:
- TypeScript Interface für Props
- cva für Variants (falls >3 Varianten)
- Accessibility-Attribute
- Theme-Token Verwendung
- Maximal 6 Utility-Klassen im Base-Style
Komponente: [BESCHREIBUNG]
## Testing und Validierung
### Storybook Integration
**src/components/Button.stories.ts**
```typescript
import type { Meta, StoryObj } from "@storybook/astro";
import Button from "./Button.astro";
const meta: Meta<typeof Button> = {
title: "Components/Button",
component: Button,
parameters: {
layout: "centered",
},
argTypes: {
variant: {
control: { type: "select" },
options: ["primary", "secondary", "ghost", "danger"],
},
size: {
control: { type: "select" },
options: ["sm", "md", "lg", "xl"],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: "primary",
size: "md",
children: "Button",
},
};
export const AllVariants: Story = {
render: () => ({
components: { Button },
template: `
<div class="space-y-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
</div>
`,
}),
};
Visual Regression Testing
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: 'tests',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
tests/components.spec.ts
import { test, expect } from '@playwright/test';
test('Button variants look correct', async ({ page }) => {
await page.goto('/storybook/button');
await expect(page.locator('[data-variant="primary"]')).toHaveScreenshot(
'button-primary.png'
);
await expect(page.locator('[data-variant="secondary"]')).toHaveScreenshot(
'button-secondary.png'
);
});
Lessons Learned
KI kann viel Zeit sparen – aber generierter Tailwind-Code ist oft Tailwind 3-Style und endet im Utility-Salat.
Die Lösung:
- Tailwind 4-Konventionen (Tokens, Utilities, Variants)
- Astro-Komponenten statt losem HTML
- Guardrails in CI, die sicherstellen, dass alle den gleichen Standard einhalten
- Systematisches Prompting für besseren KI-Output
- Automatisierte Validierung durch Linting und Testing
Damit wird KI-Code nicht zum Risiko, sondern zum Turbo – weil du ihn automatisch in dein System zwingst.
Der nächste Schritt: Implementiere die Guardrails in deinem aktuellen Projekt und teste sie mit KI-generiertem Code. Du wirst überrascht sein, wie viel sauberer der Output wird.
Mit System statt Chaos wird KI zum perfekten Coding-Partner.