CSS-Architekturen – BEM, Modules, Tailwind & CSS-in-JS
Von BEM über CSS Modules bis zu Tailwind und CSS-in-JS: Ein praxisnaher Überblick über moderne CSS-Architekturen und wann man welche Methode einsetzt.
CSS ist mächtig, aber ohne Struktur schnell chaotisch. Bei wachsenden Projekten werden globale Styles, Spezifitätskonflikte und toter Code zum Problem. Jeder Frontend-Entwickler kennt den Moment: Man ändert eine CSS-Klasse und plötzlich sieht eine komplett andere Seite anders aus. Dieser Artikel zeigt, warum das passiert und welche modernen Lösungsansätze es gibt.
Das Problem mit CSS
Spezifität – Die Wurzel allen Übels
CSS hat ein Spezifitäts-System, das bestimmt, welche Regel gewinnt wenn mehrere auf dasselbe Element zutreffen:
/* Wer gewinnt? */
.button { color: blue; } /* 0-1-0 */
.header .button { color: red; } /* 0-2-0 */
#main .button { color: green; } /* 1-1-0 ← gewinnt */
button.button { color: purple; } /* 0-1-1 */
Spezifitäts-Berechnung:
┌────────────────┬──────────┬───────────────────────────────────┐
│ Kategorie │ Gewicht │ Beispiele │
├────────────────┼──────────┼───────────────────────────────────┤
│ Inline-Styles │ 1-0-0-0 │ style="color: red" │
│ IDs │ 0-1-0-0 │ #header, #main │
│ Klassen │ 0-0-1-0 │ .button, :hover, [type="text"] │
│ Elemente │ 0-0-0-1 │ div, p, ::before │
└────────────────┴──────────┴───────────────────────────────────┘
Die vier Grundprobleme
1. Globaler Namespace → Jede CSS-Klasse gilt überall
2. Kaskadierung → Reihenfolge im Stylesheet entscheidet
3. Spezifitätskriege → !important als Eskalation
4. Toter Code → Ungenutzte Klassen schwer zu finden
Ein typischer Verlauf in Projekten ohne CSS-Architektur:
/* Phase 1: Sauberer Start */
.button { background: blue; }
/* Phase 2: Erste Anpassung */
.sidebar .button { background: green; }
/* Phase 3: Eskalation */
.sidebar .content .button { background: red !important; }
/* Phase 4: Verzweiflung */
#app .sidebar .content .button.active { background: purple !important; }
1. BEM – Block Element Modifier
BEM ist eine Namenskonvention, die CSS strukturiert und Konflikte vermeidet. Die Idee: Jede Klasse beschreibt eindeutig ihre Rolle.
Die Konvention
.block → Eigenständige Komponente
.block__element → Teil einer Komponente
.block--modifier → Variante einer Komponente
.block__element--mod → Variante eines Elements
Praktisches Beispiel
/* Block: Die Karte als eigenständige Komponente */
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
/* Elemente: Teile der Karte */
.card__header {
padding: 16px;
border-bottom: 1px solid #eee;
}
.card__title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.card__image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card__body {
padding: 16px;
}
.card__footer {
padding: 12px 16px;
background: #f9f9f9;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* Modifier: Varianten */
.card--featured {
border-color: gold;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.2);
}
.card--compact {
.card__body { padding: 8px; }
}
.card__title--large {
font-size: 1.5rem;
}
<article class="card card--featured">
<div class="card__header">
<h2 class="card__title card__title--large">Projekt XY</h2>
</div>
<img class="card__image" src="preview.jpg" alt="Preview">
<div class="card__body">
<p>Beschreibung des Projekts...</p>
</div>
<div class="card__footer">
<button class="btn btn--secondary">Details</button>
<button class="btn btn--primary">Öffnen</button>
</div>
</article>
BEM mit SCSS
.card {
border: 1px solid #ddd;
border-radius: 8px;
&__header {
padding: 16px;
border-bottom: 1px solid #eee;
}
&__title {
font-size: 1.25rem;
&--large {
font-size: 1.5rem;
}
}
&__body {
padding: 16px;
}
&--featured {
border-color: gold;
}
&--disabled {
opacity: 0.5;
pointer-events: none;
}
}
Vorteile:
- Keine Verschachtelung nötig – flache Spezifität
- Klare Zugehörigkeit – man sieht sofort, was wozu gehört
- Framework-unabhängig – funktioniert überall
- Gut für Teams – konsistente Benennung
Nachteile:
- Lange Klassennamen (
.navigation__menu-item--active) - Manuelle Disziplin erforderlich – kein Tool erzwingt BEM
- Verbosity – mehr Tipparbeit
2. CSS Modules
CSS Modules generieren automatisch einzigartige Klassennamen zur Build-Zeit. Das löst das Scoping-Problem ohne Konventionen.
Funktionsweise
/* Button.module.css */
.button {
background: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
}
.button:hover {
background: #2563eb;
}
.primary {
background: #8b5cf6;
}
.danger {
background: #ef4444;
}
.large {
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
}
// Button.jsx
import styles from './Button.module.css';
function Button({ variant = 'default', size, children, ...props }) {
const classes = [
styles.button,
variant === 'primary' && styles.primary,
variant === 'danger' && styles.danger,
size === 'large' && styles.large,
].filter(Boolean).join(' ');
return (
<button className={classes} {...props}>
{children}
</button>
);
}
// Verwendung
<Button>Standard</Button>
<Button variant="primary">Primary</Button>
<Button variant="danger" size="large">Löschen</Button>
Generiertes HTML:
<button class="Button_button_x7f3d">Standard</button>
<button class="Button_button_x7f3d Button_primary_k9j2s">Primary</button>
<button class="Button_button_x7f3d Button_danger_m3p1q Button_large_r8w2e">Löschen</button>
Composition – Styles wiederverwenden
/* shared.module.css */
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* Card.module.css */
.card {
composes: flex-center from './shared.module.css';
padding: 2rem;
background: white;
}
Vorteile:
- Automatisches Scoping – keine Konflikte möglich
- Standard-CSS-Syntax – nichts Neues lernen
- Build-Tool-Support – Vite, Webpack, Next.js
- Kein Runtime-Overhead – alles passiert zur Build-Zeit
- Colocated mit der Komponente
Nachteile:
- Kein dynamisches Styling (ohne Inline-Styles)
- Braucht einen Bundler
- Klassennamen im generierten HTML unleserlich
3. Utility-First CSS mit Tailwind
Tailwind CSS nutzt vordefinierte Utility-Klassen statt Custom CSS. Der Ansatz: Man schreibt kein CSS mehr, sondern kombiniert bestehende Klassen.
Der Paradigmenwechsel
<!-- Traditionell: CSS schreiben, Klasse zuweisen -->
<button class="btn btn--primary btn--large">Kaufen</button>
<!-- Tailwind: Utilities direkt im HTML -->
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold
py-2 px-4 rounded-lg transition-colors duration-200
active:scale-95 disabled:opacity-50">
Kaufen
</button>
Responsive Design mit Tailwind
<!-- Mobile-first: Standardmäßig 1 Spalte, ab md 2, ab lg 3 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="p-4 bg-white rounded-lg shadow">Karte 1</div>
<div class="p-4 bg-white rounded-lg shadow">Karte 2</div>
<div class="p-4 bg-white rounded-lg shadow">Karte 3</div>
</div>
<!-- Responsive Typography -->
<h1 class="text-xl md:text-3xl lg:text-5xl font-bold">
Responsive Headline
</h1>
┌──────────┬────────────┬──────────────────────┐
│ Prefix │ Breakpoint │ Entspricht │
├──────────┼────────────┼──────────────────────┤
│ (none) │ 0px+ │ Mobile │
│ sm: │ 640px+ │ Großes Smartphone │
│ md: │ 768px+ │ Tablet │
│ lg: │ 1024px+ │ Laptop │
│ xl: │ 1280px+ │ Desktop │
│ 2xl: │ 1536px+ │ Großer Monitor │
└──────────┴────────────┴──────────────────────┘
Dark Mode
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1 class="text-2xl font-bold text-blue-600 dark:text-blue-400">
Automatischer Dark Mode
</h1>
</div>
Tailwind v4 (2024+)
Die neueste Version bringt signifikante Verbesserungen:
/* CSS-first Konfiguration statt tailwind.config.js */
@import "tailwindcss";
@theme {
--color-primary: #3b82f6;
--color-accent: #8b5cf6;
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 1920px;
}
<!-- Neue Features in v4 -->
<div class="container-[800px]">
<p class="text-primary font-display">
Custom Properties direkt nutzbar
</p>
</div>
Änderungen in v4:
- CSS-first Config statt JavaScript config-Datei
- Automatische Content-Detection – kein
content-Array mehr - Oxide Engine – bis zu 10x schneller
- Native CSS Nesting statt PostCSS
Tailwind mit Komponenten-Abstraktion
Um Wiederholungen zu vermeiden:
// React: Wiederverwendbare Komponenten statt @apply
function Badge({ children, variant = 'default' }) {
const variants = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
};
return (
<span className={`inline-flex items-center px-2.5 py-0.5
rounded-full text-xs font-medium
${variants[variant]}`}>
{children}
</span>
);
}
<Badge variant="success">Online</Badge>
<Badge variant="danger">Fehler</Badge>
Vorteile:
- Extrem schnelle Entwicklung
- Konsistentes Design-System über Utility-Klassen
- Winzige Bundle-Größe (nur genutzte Klassen)
- Kein Kontextwechsel zwischen HTML und CSS
- Responsive und Dark Mode eingebaut
Nachteile:
- Längere HTML-Klassen – kann unübersichtlich werden
- Lernkurve – man muss die Klassennamen kennen
- Nicht ideal für Content-getriebene Seiten (CMS, Blog-Markdown)
- Vendor Lock-in – schwer zu migrieren
4. CSS-in-JS
Styles werden direkt in JavaScript definiert. Das ermöglicht dynamische Styles basierend auf Props, State oder Themes.
Styled Components (React)
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.$primary ? '#8b5cf6' : '#6b7280'};
color: white;
padding: ${props => props.$size === 'large' ? '12px 24px' : '8px 16px'};
border: none;
border-radius: 8px;
font-size: ${props => props.$size === 'large' ? '1.1rem' : '0.9rem'};
cursor: pointer;
transition: all 0.2s;
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
const Card = styled.article`
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
/* Verschachtelte Styles */
h2 {
font-size: 1.5rem;
margin-bottom: 8px;
}
p {
color: #666;
line-height: 1.6;
}
`;
// Verwendung
<Card>
<h2>Titel</h2>
<p>Inhalt der Karte</p>
<Button $primary $size="large">Action</Button>
</Card>
Emotion
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const cardStyle = css`
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
`;
function Card({ highlighted, children }) {
return (
<div css={[
cardStyle,
highlighted && css`border: 2px solid gold;`
]}>
{children}
</div>
);
}
Vanilla Extract (Zero-Runtime)
Der entscheidende Unterschied: Vanilla Extract generiert CSS zur Build-Zeit, nicht zur Laufzeit.
// button.css.ts
import { style, styleVariants } from '@vanilla-extract/css';
export const base = style({
padding: '8px 16px',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
':hover': {
transform: 'translateY(-1px)',
}
});
export const variants = styleVariants({
primary: { background: '#8b5cf6', color: 'white' },
secondary: { background: '#e5e7eb', color: '#374151' },
danger: { background: '#ef4444', color: 'white' },
});
// Button.tsx
import { base, variants } from './button.css';
function Button({ variant = 'primary', children }) {
return (
<button className={`${base} ${variants[variant]}`}>
{children}
</button>
);
}
Vergleich der CSS-in-JS Lösungen
┌──────────────────┬─────────────┬───────────┬────────────┬──────────────┐
│ │ Styled Comp.│ Emotion │ Vanilla Ex.│ CSS Modules │
├──────────────────┼─────────────┼───────────┼────────────┼──────────────┤
│ Runtime-Overhead │ Ja (~12KB) │ Ja (~7KB) │ Nein │ Nein │
│ TypeScript │ ✅ │ ✅ │ ✅ (Native)│ ⚠️ │
│ Dynamic Styles │ ✅ │ ✅ │ ⚠️ Limited │ ❌ │
│ SSR-Support │ ✅ │ ✅ │ ✅ │ ✅ │
│ Bundle Impact │ Mittel │ Mittel │ Kein │ Kein │
│ Framework │ React │ React │ Alle │ Alle │
└──────────────────┴─────────────┴───────────┴────────────┴──────────────┘
Trend 2024+: Weg von Runtime-CSS-in-JS (Styled Components, Emotion) hin zu Zero-Runtime-Lösungen (Vanilla Extract, CSS Modules). React Server Components haben diesen Shift beschleunigt, da CSS-in-JS-Bibliotheken mit RSC teilweise inkompatibel sind.
5. Native CSS-Features (2024+)
Moderne Browser unterstützen Features, die früher externe Tools erforderten. Das ist der vielleicht wichtigste Trend in der CSS-Welt.
CSS Nesting (Native!)
/* Früher nur mit Sass/Less/PostCSS – jetzt nativ! */
.card {
background: white;
border-radius: 12px;
padding: 24px;
/* Verschachtelte Selektoren */
& .title {
font-size: 1.5rem;
font-weight: 600;
}
& .description {
color: #666;
line-height: 1.6;
}
/* Pseudo-Klassen */
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
/* Media Queries direkt im Block */
@media (width >= 768px) {
padding: 32px;
display: grid;
grid-template-columns: 1fr 2fr;
}
}
CSS Cascade Layers
Layers lösen Spezifitätsprobleme auf einer höheren Ebene:
/* Layer-Reihenfolge definieren (letzter gewinnt) */
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer base {
h1 { font-size: 2rem; }
a { color: blue; }
}
@layer components {
.card { padding: 1rem; }
.button { background: blue; color: white; }
}
@layer utilities {
.mt-4 { margin-top: 1rem; }
.text-center { text-align: center; }
}
/* Utilities gewinnen IMMER über Components,
unabhängig von der Spezifität der Selektoren! */
Ohne Layers: Spezifität entscheidet
#id > .class > element
Mit Layers: Layer-Reihenfolge entscheidet ZUERST
utilities > components > base > reset
Innerhalb eines Layers: Spezifität
→ Ein einfaches .mt-4 in @layer utilities schlägt
ein #main .sidebar .card in @layer components
Container Queries
Der langersehnte Durchbruch: Styles basierend auf der Container-Größe statt der Viewport-Größe.
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
/* Standard: Kompaktes Layout */
display: flex;
flex-direction: column;
gap: 8px;
}
/* Ab 400px Container-Breite: Horizontal */
@container card (width >= 400px) {
.card {
flex-direction: row;
gap: 16px;
}
.card__image {
width: 40%;
}
}
/* Ab 600px: Noch mehr Platz */
@container card (width >= 600px) {
.card {
gap: 24px;
}
.card__title {
font-size: 1.5rem;
}
}
Warum Container Queries besser als Media Queries sind:
Media Query: "Ist der Bildschirm breit genug?"
Container Query: "Ist der Container breit genug?"
→ Eine Karte in der Sidebar verhält sich anders
als dieselbe Karte im Hauptbereich – automatisch!
:has() Selector – Der “Eltern-Selektor”
Der wohl mächtigste neue CSS-Selektor:
/* Formulare, die ungültige Felder enthalten */
.form:has(:invalid) {
border-color: red;
}
/* Cards mit Bildern bekommen kein Padding oben */
.card:has(img) {
padding-top: 0;
}
/* Navigation hervorheben wenn aktiver Link drin ist */
.nav-item:has(.active) {
background: rgba(0,0,0,0.1);
}
/* Label-Styling basierend auf Checkbox-State */
label:has(input[type="checkbox"]:checked) {
color: green;
text-decoration: line-through;
}
/* Grid-Layout anpassen basierend auf Kinderzahl */
.grid:has(:nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(:nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
Weitere moderne CSS-Features
/* Subgrid – Kinder am Eltern-Grid ausrichten */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.grid-item {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}
/* View Transitions API */
::view-transition-old(root) {
animation: slide-out 0.3s ease-out;
}
::view-transition-new(root) {
animation: slide-in 0.3s ease-in;
}
/* Color Mix */
.blend {
background: color-mix(in srgb, blue 70%, white);
}
/* Logical Properties */
.card {
margin-inline: auto; /* links und rechts */
padding-block: 1rem; /* oben und unten */
border-inline-start: 3px solid blue; /* links (LTR) */
}
Gesamtvergleich
┌───────────────────┬──────────┬──────────┬───────────┬──────────┬────────────┐
│ Ansatz │ Scoping │ Bundle │ Lernkurve │ Dynamik │ Framework │
├───────────────────┼──────────┼──────────┼───────────┼──────────┼────────────┤
│ BEM │ Manuell │ Klein │ Niedrig │ ❌ │ Alle │
│ CSS Modules │ Auto │ Klein │ Niedrig │ ❌ │ Alle │
│ Tailwind │ Auto │ Minimal │ Mittel │ ⚠️ │ Alle │
│ Styled Components │ Auto │ Mittel │ Mittel │ ✅ │ React │
│ Vanilla Extract │ Auto │ Klein │ Hoch │ ⚠️ │ Alle │
│ Native CSS │ Layers │ Kleinste │ Niedrig │ ⚠️ │ Alle │
└───────────────────┴──────────┴──────────┴───────────┴──────────┴────────────┘
Empfehlungen nach Projekttyp
Kleine Projekte / Prototypen
→ Tailwind CSS für schnelle Entwicklung, oder Native CSS mit Nesting und Layers
Komponentenbibliotheken
→ CSS Modules oder Vanilla Extract für isolierte, typisierte Styles
Design-System-Teams
→ Tailwind als Basis + CSS Modules für komplexe Komponenten
Content-Seiten / Blogs
→ BEM + SCSS oder Native CSS – Markdown-Content braucht globale Styles
Server-Side Rendering (Next.js, Remix)
→ Vanilla Extract oder CSS Modules (Zero Runtime)
Legacy-Projekte modernisieren
→ CSS Layers einführen, dann schrittweise BEM → CSS Modules migrieren
Mein persönlicher Stack
Für diesen Blog (Hugo + Static Site): BEM-Methodik mit SCSS. Bei einer statischen Seite ohne JavaScript-Framework sind CSS Modules oder Tailwind unnötig komplex. SCSS bietet Nesting, Variablen und Mixins – genau das richtige für Template-basiertes Styling.
Für React-Projekte: CSS Modules als Standard, Tailwind für Utilities, native CSS-Features wo möglich.
Fazit
Es gibt keine “beste” CSS-Architektur – die Wahl hängt vom Projekt, Team und Toolchain ab. Der größte Trend 2024+: Native CSS wird immer mächtiger. Nesting, Layers, Container Queries und :has() machen viele Tools überflüssig. Für große Projekte mit Komponenten-Architektur bleiben CSS Modules und Tailwind die pragmatischsten Lösungen.