In der Frontend-Entwicklung gibt es zwei Interessengruppen. Nutzer wollen eine Seite, die gut aussieht, schnell lädt und sich vorhersehbar verhält. Entwickler wollen Code, der lesbar und wartbar bleibt. Die meisten Lösungen bevorzugen die eine Seite auf Kosten der anderen.

Ich setze auf Tailwind CSS wegen seines Utility-first-Tempos, nehme aber die Kritik von Frameworks wie Nuejs ernst: Berge von Utility-Klassen begraben die semantische Struktur deines HTML. Dieser Beitrag zeigt, wie ich beides mit CSS-Layern versöhne - Tailwinds Flexibilität, ohne sauberes Markup aufzugeben.

Was ein Design trägt

Gutes Design läuft auf ein paar Einschränkungen hinaus, die willkürliche Entscheidungen stoppen, bevor sie sich vervielfachen - das Gegenmittel zum Over-Engineering, das moderne Frontends plagt:

  • Eine begrenzte Farbpalette - fünf bis sieben Kernfarben mit klaren semantischen Rollen (Marke, Neutral, Success/Warning/Error, Text). Eingeschränkte Paletten halten den Kontrast konsistent und barrierefrei.
  • Typografie-Einschränkungen - ein Font-Stack mit klarer Hierarchie. Der visuelle Rhythmus ergibt sich von selbst, Nutzer erfassen die Struktur schneller.
  • Ein kleiner Satz Komponenten - eine standardisierte Bibliothek schlägt einen Wildwuchs ähnlicher-aber-anderer Elemente, die Nutzer verwirren und den Wartungsaufwand aufblähen.
  • Eine mathematische Abstandsskala - systematische Schritte (Zweierpotenzen funktionieren gut) statt willkürlicher Pixelwerte.

Zwei weitere Dinge zählen und kosten fast nichts. Semantisches HTML liefert Barrierefreiheit und SEO von Haus aus - Screenreader und Suchmaschinen verstehen <nav>, <article> und <button> bereits, eigene Komponenten werfen das weg. Und Responsiveness ist nicht verhandelbar: Mobil macht über 60% des Web-Traffics aus, Desktop rund ein Drittel, Tablets ein Bruchteil - auch wenn sich der Mix je nach Branche stark verschiebt (Restaurants tendieren mobil, SaaS tendiert Desktop).

Wo Tailwind glänzt, und wo es an Grenzen stößt

Tailwinds Preflight normalisiert Browser-Unterschiede, und von dieser Basis aus stylst du direkt im Markup, ohne Datei-Wechsel. Kompression erledigt die langen Klassenlisten in Produktion, die Performance-Sorge ist also übertrieben.

Der eigentliche Preis ist Lesbarkeit. Utility-lastiges HTML verschleiert seine eigene Struktur und lädt still zu Inkonsistenz ein - leicht abweichende Klassenkombinationen schleichen sich über Komponenten hinweg ein. Der Schmerz ist in Template-Systemen wie Django größer, wo Wiederverwendung bewussten Aufwand braucht ({% include %}), als in Komponenten-Frameworks wie Svelte oder React, die von Haus aus kapseln.

Das Gegenargument von Nuejs

Nuejs lehnt Utility-first komplett ab und setzt stattdessen auf modernes CSS: @layer für die Kontrolle der Kaskade, Custom Properties fürs Theming, calc() und natives Nesting. Statt Tailwinds vollem Reset normalisiert es minimal, sodass semantische Elemente ihr natürliches Styling behalten:

normalize.css
@layer settings {
*,
*::before,
*::after {
box-sizing: border-box;
}
form {
button,
input,
select,
textarea {
font: inherit; /* Match body text */
}
}
}

Es bevorzugt außerdem semantische Klassennamen mit minimaler Verschmutzung - ein Modifier auf einer Basiskomponente statt einer Wand aus Utilities:

notification.html
<div class="notification card">
<h3>ChitChat</h3>
<p>New message</p>
</div>
components/card.css
@layer components {
.card {
box-shadow: 0 0 2em #0001;
border: var(--border);
border-radius: 0.5em;
padding: 1.5em;
font-size: 95%;
&.notification {
background: url(/img/chat.svg) 10% center no-repeat;
background-size: 3rem;
padding-left: 6rem;
}
}
}

Und es stylt anhand von ARIA-Attributen statt Klassen, was dich als Nebeneffekt zu barrierefreiem Markup schubst:

components/accordion.css
.accordion[aria-expanded="true"] {
max-height: 100%;
}
.accordion[aria-expanded="false"] {
max-height: 0;
}

Der Ton ist scharf, aber die Sorge um langfristige Wartbarkeit ist berechtigt.

Die Versöhnung

Ich will mich nicht auf eine Seite schlagen. Tailwind hat reines CSS nie verboten - es unterstützt Komponenten-Extraktion aktiv über @apply und CSS-Nesting. Also organisiere ich das gesamte Stylesheet mit Layern und nutze jedes Werkzeug dort, wo es passt:

main.css
@import "tailwindcss";
@import "./theme.css" layer(theme);
@import "./base.css" layer(base);
@import "./components.css" layer(components);
@import "./utilities.css" layer(utilities);
@custom-variant dark (&:where(.dark, .dark *));
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";

Open in StackBlitz

Vier Layer, jeder mit einer Aufgabe: ein Theme, Basis-Styles für Elemente, wiederverwendbare Komponenten und eigene Utilities. Die Plugins Forms und Typography legen gut getestete, barrierefreie Defaults obendrauf.

Theme

theme.css ist die einzige Quelle der Wahrheit für das Designsystem - Farben, Abstände, Typografie. Farben in :root zu definieren (mit einer dunklen @variant) und sie dann in @theme zu mappen, lässt einen Variablen-Tausch das ganze Theme umstellen:

theme.css
:root {
--foreground: oklch(0.15 0.01 270); /* Rich dark */
--background: oklch(0.99 0.002 270); /* Pure white with hint */
--primary: oklch(0.55 0.18 262); /* Rich blue */
--secondary: oklch(0.6 0.14 285); /* Purple-blue */
--success: oklch(0.7 0.16 142); /* Fresh green */
--error: oklch(0.65 0.2 25); /* Vibrant red */
--warning: oklch(0.8 0.15 75); /* Warm amber */
--muted: oklch(0.55 0.03 270); /* Neutral gray */
@variant dark {
--foreground: oklch(0.95 0.01 270); /* Near white with warmth */
--background: oklch(0.05 0.002 270); /* Deep dark with blue hint */
--primary: oklch(0.7 0.15 262); /* Brighter blue for contrast */
--secondary: oklch(0.72 0.12 285); /* Lighter purple-blue */
--success: oklch(0.75 0.14 142); /* Brighter green */
--warning: oklch(0.85 0.13 75); /* Brighter amber */
--error: oklch(0.7 0.18 25); /* Softer red */
--muted: oklch(0.45 0.02 270); /* Muted gray for dark mode */
}
}
@theme {
--color-primary: var(--primary);
--color-secondary: var(--secondary);
--color-success: var(--success);
--color-error: var(--error);
--color-warning: var(--warning);
--color-foreground: var(--foreground);
--color-background: var(--background);
--color-muted: var(--muted);
/* Typography system */
--font-sans:
Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro,
sans-serif;
--font-serif:
Rockwell, "Rockwell Nova", "Roboto Slab", "DejaVu Serif", "Sitka Small",
serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}

Das Präfix --color-* ist wichtig: Tailwind erzeugt Utilities wie bg-background und text-foreground aus Variablen, die so benannt sind.

Basis-Styles

base.css setzt Defaults für semantische HTML-Elemente - Hintergrund, Textfarbe, Überschriften, Links. Es hält die Darstellung konsistent, während das HTML aussagekräftig bleibt:

base.css
/* Minimal base styles */
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground font-sans;
}

Komponenten

In components.css leben die wiederverwendbaren Muster, die Tailwind-Utilities dort mit eigenem CSS mischen, wo es nötig ist:

components/button.css
/* Button components */
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors;
@apply focus:outline-2 focus:outline-offset-2 focus:outline-primary;
&.btn-primary {
@apply bg-primary text-white hover:bg-primary/90;
}
&.btn-secondary {
@apply bg-secondary text-white hover:bg-secondary/90;
}
&:disabled {
@apply opacity-50 cursor-not-allowed;
}
}

Bei größeren Projekten teilst du das in eine Datei pro Komponente auf:

components/index.css
@import "./button.css";
@import "./card.css";
@import "./form.css";
@import "./navigation.css";

Utilities

utilities.css enthält eigene Utilities für die Lücken, die Tailwind nicht abdeckt:

utilities.css
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
/* Screen reader only utility */
.sr-only {
@apply absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0;
clip: rect(0, 0, 0, 0);
}

Weil jeder Style in genau einem Layer und an genau einem Ort liegt, wird die Wahl zwischen reinem CSS und einer Utility zur Frage des Kontexts, nicht der Architektur.

Fazit

Bootstrap ist am schnellsten, wenn du Standardkomponenten brauchst; es wird einengend, sobald die Marke Spezifisches verlangt. Tailwind kauft diese Flexibilität zurück, auf Kosten der HTML-Lesbarkeit. DaisyUI sitzt dazwischen, und sein Modell aus semantischer Komponente plus Utility spiegelt das hier gezeigte Layer-Setup.

Das Hybrid gewinnt, weil es die Belange trennt: CSS besitzt die Darstellung, HTML besitzt Struktur und Barrierefreiheit. Du bekommst Tailwinds Tempo und eine Codebasis, die sich auch ein Jahr später noch sauber liest.

Im Kern sollte es bei Webdesign um Worte gehen. Worte kommen nicht, nachdem das Design fertig ist. Worte sind der Anfang, der Kern, der Fokus.

-- Words

Siehe auch: Motherfucking Website - eine satirische Kritik an over-engineerter Webentwicklung.