Zum Hauptinhalt springen
Wenn KI Tailwind-Code schreibt: Vom Utility-Salat zu sauberen Variants in Tailwind 4
#KI #Tailwind #Code-Quality #Astro #Automatisierung

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 @utility extrahieren
  • 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:

  1. TypeScript Interface für Props
  2. cva für Variants (falls >3 Varianten)
  3. Accessibility-Attribute
  4. Theme-Token Verwendung
  5. 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:

  1. Tailwind 4-Konventionen (Tokens, Utilities, Variants)
  2. Astro-Komponenten statt losem HTML
  3. Guardrails in CI, die sicherstellen, dass alle den gleichen Standard einhalten
  4. Systematisches Prompting für besseren KI-Output
  5. 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.