11 Min. Lesezeit

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.