Component library with 45+ Astro components in Atomic Design
15 min read

Atomic Design in der Praxis: Ein Portfolio mit 45+ Astro-Komponenten

#Atomic Design #Astro #Component Architecture

Du kennst die Theorie hinter Atomic Design — Atome, Moleküle, Organismen, Templates, Seiten. Davon haben wir alle gelesen, Diagramme gezeichnet und genickt. Aber was passiert, wenn du diese Methodik tatsächlich in einem echten Projekt umsetzt? Nicht in einem Tutorial mit drei Komponenten, sondern in einem mehrsprachigen Portfolio mit über 45 Komponenten, 3 Layouts und 106 generierten Seiten?

Genau das habe ich getan. Und dabei habe ich Dinge gelernt, die in keinem Theorie-Artikel stehen. Dieses Post zeigt dir die konkreten Entscheidungen, die echte Ordnerstruktur und die Muster, die sich nach Monaten produktiver Arbeit bewährt haben.

Atomic Design klingt in der Theorie simpel. Die echte Herausforderung beginnt, wenn du entscheiden musst, ob etwas ein Atom oder ein Molekül ist — und das 45 Mal hintereinander.

Arnold Wender Webentwickler & Digitaler Creator

Die Zahlen: Was tatsächlich entstanden ist

Bevor wir in den Code eintauchen, hier der Überblick darüber, was bei der konsequenten Anwendung von Atomic Design auf ein reales Projekt herausgekommen ist.

12
Atome
17
Moleküle
16
Organismen
3
Layouts
106
Seiten

Diese Zahlen sind kein Zufall. Die Verteilung — viele Atome und Moleküle, weniger Organismen — spiegelt die natürliche Pyramide wider, die bei konsistenter Anwendung von Atomic Design entsteht. Die Basis ist breit (viele kleine, wiederverwendbare Teile), die Spitze ist schmal (wenige große, spezialisierte Sektionen). Wenn deine Architektur umgekehrt aussieht — viele Organismen, wenig Atome — stimmt etwas mit der Abstraktion nicht.

Die echte Ordnerstruktur

Theorie-Artikel zeigen dir eine schöne Ordnerstruktur mit fünf Ordnern und je drei Dateien. Realität sieht anders aus. Hier ist die tatsächliche Struktur meines Portfolios — jede einzelne Datei.

src/components/atoms/
├── Badge.astro          # Farbige Tags für Kategorien und Status
├── Button.astro         # Primär/Sekundär/Ghost, <a> oder <button>
├── Heading.astro        # h1–h6, Größe unabhängig vom Level
├── Icon.astro           # 68+ SVG-Pfade in einer Komponente
├── Image.astro          # Lazy-Loading, Aspect-Ratio, Fallback
├── Input.astro          # Text/E-Mail/Search mit Validierung
├── Link.astro           # Interner/externer Link mit Icon-Support
├── Tag.astro            # Klickbare Filter-Tags
├── Text.astro           # Fließtext mit Größen- und Gewichtsvarianten
├── Textarea.astro       # Mehrzeiliges Eingabefeld
├── ThemeToggle.astro    # Dark/Light-Mode-Schalter
└── blog/
    └── ReadingProgress.astro  # Fortschrittsbalken beim Scrollen
src/components/molecules/
├── Breadcrumbs.astro        # Pfadnavigation + Schema.org
├── Card.astro               # Universelle Karte: Heading + Text + Button
├── ContactInfo.astro        # Adresse/Telefon/E-Mail-Block
├── FormField.astro          # Label + Input + Fehlermeldung
├── LanguageSwitcher.astro   # DE/EN/ES-Sprachumschalter
├── NavLink.astro            # Navigation-Link mit Active-State
├── Pagination.astro         # Seitennavigation mit Prev/Next
├── ProjectCard.astro        # Portfolio-Karte mit Tags
├── SearchBar.astro          # Input + Button + Suchlogik
├── SocialLinks.astro        # Icon-Links zu Social Media
├── TestimonialCard.astro    # Kundenmeinung mit Avatar
├── TimelineItem.astro       # Einzelner Zeitstrahl-Eintrag
├── TrustBadge.astro         # Vertrauens-Indikatoren
└── blog/
    ├── CalloutBox.astro     # CTA-Box im Artikel
    ├── InfoBox.astro        # Hinweis/Warnung/Tipp-Box
    ├── QuoteBlock.astro     # Zitat mit Quellenangabe
    └── StatCard.astro       # Kennzahl mit Label
src/components/organisms/
├── About.astro              # Über-mich-Sektion
├── Contact.astro            # Kontaktformular komplett
├── FaqSection.astro         # FAQ mit Akkordeon
├── Footer.astro             # Kompletter Footer
├── Header.astro             # Header mit Navigation
├── Hero.astro               # Hero-Sektion mit Varianten
├── Navigation.astro         # Hauptnavigation (Desktop/Mobile)
├── Portfolio.astro          # Projektgalerie
├── Pricing.astro            # Preisübersicht
├── Services.astro           # Leistungsübersicht
├── Testimonials.astro       # Kundenstimmen-Karussell
├── TimelineSection.astro    # Berufserfahrung-Zeitstrahl
└── blog/
    ├── CodeTabs.astro       # Code-Tabs für MDX
    ├── ComparisonTable.astro # Vergleichstabelle
    ├── FaqAccordion.astro   # FAQ im Blog-Artikel
    └── ...                  # Weitere Blog-Organismen
src/layouts/
├── BaseLayout.astro         # Grundgerüst: Head + Header + Main + Footer
├── BlogLayout.astro         # Blog-spezifisch: TOC + Author + Related
└── PortfolioLayout.astro    # Portfolio-Detail: Galerie + Technik

src/pages/
├── index.astro              # Startseite (Hero + Services + Portfolio + ...)
├── about.astro              # Über mich
├── contact.astro            # Kontakt
├── pricing.astro            # Preise
├── blog/
│   ├── index.astro          # Blog-Übersicht
│   ├── [slug].astro         # Einzelner Blog-Post
│   ├── page/[page].astro    # Paginierung
│   ├── tags.astro           # Tag-Übersicht
│   ├── tag/[tag].astro      # Posts pro Tag
│   ├── categories.astro     # Kategorie-Übersicht
│   ├── kategorie/[cat].astro # Posts pro Kategorie
│   └── search.astro         # Blog-Suche
└── portfolio/
    ├── index.astro          # Portfolio-Übersicht
    └── [slug].astro         # Portfolio-Detail

Beachte die blog/-Unterordner innerhalb von atoms/, molecules/ und organisms/. Blog-spezifische Komponenten wie InfoBox, CodeTabs oder ReadingProgress leben dort getrennt von den allgemeinen UI-Komponenten. Das verhindert, dass die Hauptordner mit kontextspezifischen Komponenten überladen werden, und macht klar, welche Teile nur im Blog-Kontext Sinn ergeben.

Atome, die alles antreiben

Atome sind die Fundamente deines Design-Systems. Wenn ein Atom schlecht designt ist, spürst du das in jeder Komponente darüber. Hier sind vier Atome, die ich besonders sorgfältig gebaut habe — und warum.

Icon.astro Komponente — Code-Editor mit Props-Interface und Icon-Bibliothek mit 12 SVG-Vorschauen
Icon.astro — 68+ SVGs in einer Komponente

Statt 68 separate Icon-Dateien zu verwalten, speichert Icon.astro alle SVG-Pfade in einem einzigen switch-Statement. Das Atom akzeptiert einen name-Prop vom Typ IconName (ein TypeScript-Union-Type) und rendert den passenden SVG-Pfad. Größe und Farbe werden über Props gesteuert. Dieses eine Atom wird von NavLink, Button, SocialLinks, ContactInfo und dutzenden weiteren Komponenten importiert. Eine Änderung am Icon-System wirkt sich sofort überall aus.

Button.astro Komponente — Code-Editor mit Props-Interface und drei Button-Varianten: Primary, Secondary, Ghost
Button.astro — Semantisches HTML über Props

Button.astro rendert entweder ein <a>-Element (wenn ein href-Prop vorhanden ist) oder ein <button>-Element — entschieden zur Build-Zeit. Dazu kommen variant (primary/secondary/ghost), size (sm/md/lg), und ein optionaler icon-Prop. Das class:list-Attribut von Astro kombiniert die CSS-Klassen bedingt. Ein einziges Atom, das sowohl Navigation als auch Formulare abdeckt, ohne semantisches HTML zu opfern.

Heading.astro Komponente — visuelle Hierarchie von h1 bis h6 mit entkoppelten semantischen Leveln und visuellen Groessen
Heading.astro — Semantik und Optik entkoppelt

Die häufigste Falle in Design-Systemen: h2 sieht immer gleich aus wie h2. Heading.astro trennt das Level (h1-h6, für Semantik und Zugänglichkeit) vom visuellen Size (sm/md/lg/xl/2xl). So kann ein semantisches <h3> visuell so groß wie ein <h1> dargestellt werden — zum Beispiel wenn eine Sektion tief verschachtelt ist, der Titel aber prominent sein soll.

Text.astro Komponente — Code-Editor mit Props und visueller Katalog: gross fett, normal, klein gedaempft
Text.astro — Konsistente Typographie überall

Jeder Fließtext im Projekt läuft durch Text.astro. Props: size (sm/base/lg), weight (normal/medium/bold), muted (boolean für Sekundärtext), und ein as-Prop für das HTML-Element (p/span/div). Design Tokens für Schriftgröße, Zeilenabstand und Farbe sorgen dafür, dass ein Wechsel der Typographie in tokens/typography.css sofort das gesamte Projekt aktualisiert.

Icon.astro im Detail

Weil Icon.astro von praktisch jeder anderen Komponente genutzt wird, ist sein Design besonders wichtig. Hier ein vereinfachter Blick auf die Struktur:

---
/* Icon-Atom: Rendert SVGs basierend auf einem Name-Prop */
import type { IconName } from '../../types';

interface Props {
  name: IconName;
  size?: number;
  class?: string;
  label?: string;    /* Für Barrierefreiheit, wenn Icon alleine steht */
}

const { name, size = 24, class: className, label } = Astro.props;

/* aria-hidden wenn rein dekorativ, aria-label wenn bedeutungstragend */
const isDecorative = !label;
---

<svg
  xmlns="http://www.w3.org/2000/svg"
  width={size}
  height={size}
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
  class:list={['icon', className]}
  aria-hidden={isDecorative}
  aria-label={label}
  role={label ? 'img' : undefined}
>
  {/* 68+ SVG-Pfade über switch(name) */}
</svg>
---
/* NavLink-Molekül: Nutzt Icon-Atom für optionales Leading-Icon */
import Icon from '../atoms/Icon.astro';
import Link from '../atoms/Link.astro';

interface Props {
  href: string;
  icon?: IconName;
  isActive?: boolean;
  locale?: 'de' | 'en' | 'es';
}
---

<Link
  href={href}
  class:list={['nav-link', { 'nav-link--active': isActive }]}
>
  {icon && <Icon name={icon} size={18} />}
  <slot />
</Link>

Moleküle: Atome komponieren

Ein Molekül ist eine Gruppe von Atomen, die zusammen einen einzigen Zweck erfüllen. Das ist der Schlüsseltest: Wenn du die Gruppe beschreiben kannst mit “es ist ein X”, ist es ein Molekül. “Es ist eine Karte.” “Es ist ein Navigationslink.” “Es ist ein Suchfeld.”

Card = Heading + Text + Button + Icon

Die Card-Komponente zeigt perfekt, wie Moleküle funktionieren. Sie importiert vier Atome und arrangiert sie in einem semantischen Layout:

---
/* Card-Molekül: Komponiert Atome zu einer wiederverwendbaren Karte */
import Heading from '../atoms/Heading.astro';
import Text from '../atoms/Text.astro';
import Button from '../atoms/Button.astro';
import Icon from '../atoms/Icon.astro';

interface Props {
  title: string;
  description: string;
  icon?: IconName;
  href?: string;
  buttonText?: string;
}

const { title, description, icon, href, buttonText } = Astro.props;
---

<article class="card">
  {icon && (
    <div class="card__icon">
      <Icon name={icon} size={32} />
    </div>
  )}
  <Heading level={3} size="md">{title}</Heading>
  <Text size="sm" muted>{description}</Text>
  {buttonText && href && (
    <Button variant="ghost" href={href} size="sm">
      {buttonText}
    </Button>
  )}
</article>
---
/* Breadcrumbs-Molekül: Links mit Schema.org-Markup */
import Link from '../atoms/Link.astro';
import Icon from '../atoms/Icon.astro';

interface Props {
  items: BreadcrumbItem[];
}

const { items } = Astro.props;
---

<nav aria-label="Breadcrumb" class="breadcrumbs">
  <ol itemscope itemtype="https://schema.org/BreadcrumbList">
    {items.map((item, index) => (
      <li itemprop="itemListElement" itemscope
          itemtype="https://schema.org/ListItem">
        {item.href ? (
          <Link href={item.href} itemprop="item">
            <span itemprop="name">{item.label}</span>
          </Link>
        ) : (
          <span itemprop="name" aria-current="page">
            {item.label}
          </span>
        )}
        <meta itemprop="position" content={String(index + 1)} />
        {index < items.length - 1 && (
          <Icon name="chevron-right" size={14} />
        )}
      </li>
    ))}
  </ol>
</nav>

<!-- Schema.org BreadcrumbList JSON-LD wird automatisch generiert -->
---
/* FormField-Molekül: Label + Input + Fehlermeldung als Einheit */
import Input from '../atoms/Input.astro';
import Text from '../atoms/Text.astro';

interface Props {
  label: string;
  name: string;
  type?: 'text' | 'email' | 'tel';
  required?: boolean;
  error?: string;
  helpText?: string;
}

const { label, name, type = 'text', required, error, helpText } = Astro.props;
const inputId = `field-${name}`;
---

<div class:list={['form-field', { 'form-field--error': error }]}>
  <label for={inputId} class="form-field__label">
    {label}
    {required && <span aria-hidden="true">*</span>}
  </label>
  <Input
    type={type}
    name={name}
    id={inputId}
    required={required}
    aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
    aria-invalid={!!error}
  />
  {error && (
    <Text id={`${inputId}-error`} size="sm" class="form-field__error" role="alert">
      {error}
    </Text>
  )}
  {helpText && !error && (
    <Text id={`${inputId}-help`} size="sm" muted>
      {helpText}
    </Text>
  )}
</div>

Ein besonders interessantes Molekül in einem mehrsprachigen Projekt: NavLink muss wissen, ob der aktuelle Pfad zum Link passt — und das unter Berücksichtigung des Locale-Prefixes. /en/blog und /de/blog sind beide “Blog”, aber nur einer sollte als aktiv markiert sein.

Organismen: Die Kraftpakete

Organismen sind eigenständige Sektionen, die sich wie echte Teile einer Website anfühlen. Ein Header ist ein Organismus. Ein Footer ist ein Organismus. Eine Hero-Sektion ist ein Organismus. Sie orchestrieren Moleküle und bestimmen deren Layout.

---
/* Header-Organismus: Orchestriert Navigation, Sprachumschalter, Theme */
import NavLink from '../molecules/NavLink.astro';
import LanguageSwitcher from '../molecules/LanguageSwitcher.astro';
import ThemeToggle from '../atoms/ThemeToggle.astro';
import Navigation from './Navigation.astro';
import Icon from '../atoms/Icon.astro';
import Button from '../atoms/Button.astro';

interface Props {
  locale?: 'de' | 'en' | 'es';
  currentPath: string;
}

const { locale = 'de', currentPath } = Astro.props;
const navItems = SITE.navigation;
---

<header class="header" id="header">
  <div class="header__inner">
    <!-- Logo -->
    <a href={`/${locale}`} class="header__logo" aria-label="Zur Startseite">
      <Icon name="logo" size={32} />
    </a>

    <!-- Desktop Navigation -->
    <Navigation items={navItems} locale={locale} currentPath={currentPath} />

    <!-- Aktions-Bereich -->
    <div class="header__actions">
      <LanguageSwitcher locale={locale} currentPath={currentPath} />
      <ThemeToggle />
      <Button variant="primary" size="sm" href={`/${locale}/contact`}>
        Kontakt
      </Button>
    </div>

    <!-- Mobile Menu Toggle -->
    <button class="header__menu-toggle" aria-label="Menü öffnen">
      <Icon name="menu" size={24} />
    </button>
  </div>
</header>
---
/* Footer-Organismus: Zusammensetzung aus Heading, Text, SocialLinks */
import Heading from '../atoms/Heading.astro';
import Text from '../atoms/Text.astro';
import Link from '../atoms/Link.astro';
import SocialLinks from '../molecules/SocialLinks.astro';

interface Props {
  locale?: 'de' | 'en' | 'es';
}

const { locale = 'de' } = Astro.props;
const currentYear = new Date().getFullYear();

/* Locale-spezifische Footer-Links */
const footerNav = SITE.footerNavigation[locale];
---

<footer class="footer" id="footer">
  <div class="footer__inner">
    <div class="footer__brand">
      <Heading level={2} size="md">Arnold Wender</Heading>
      <Text muted>Webentwickler & Digitaler Creator</Text>
      <SocialLinks />
    </div>

    <nav class="footer__nav" aria-label="Footer-Navigation">
      {footerNav.columns.map((column) => (
        <div class="footer__column">
          <Heading level={3} size="sm">{column.title}</Heading>
          <ul>
            {column.links.map((link) => (
              <li>
                <Link href={link.href}>{link.label}</Link>
              </li>
            ))}
          </ul>
        </div>
      ))}
    </nav>

    <div class="footer__bottom">
      <Text size="sm" muted>
        &copy; {currentYear} Arnold Wender. Alle Rechte vorbehalten.
      </Text>
    </div>
  </div>
</footer>
---
/* Hero-Organismus: Flexible Hero mit zentrierter/linksbündiger Variante */
import Heading from '../atoms/Heading.astro';
import Text from '../atoms/Text.astro';
import Button from '../atoms/Button.astro';
import Badge from '../atoms/Badge.astro';

interface Props {
  title: string;
  subtitle?: string;
  badge?: string;
  variant?: 'centered' | 'left';
  primaryCta?: { text: string; href: string };
  secondaryCta?: { text: string; href: string };
}

const {
  title,
  subtitle,
  badge,
  variant = 'centered',
  primaryCta,
  secondaryCta,
} = Astro.props;
---

<section class:list={['hero', `hero--${variant}`]} id="hero">
  <div class="hero__content">
    {badge && <Badge variant="accent">{badge}</Badge>}
    <Heading level={1} size="2xl">{title}</Heading>
    {subtitle && <Text size="lg" class="hero__subtitle">{subtitle}</Text>}
    <div class="hero__actions">
      {primaryCta && (
        <Button variant="primary" size="lg" href={primaryCta.href}>
          {primaryCta.text}
        </Button>
      )}
      {secondaryCta && (
        <Button variant="secondary" size="lg" href={secondaryCta.href}>
          {secondaryCta.text}
        </Button>
      )}
    </div>
  </div>
  <slot />  {/* Für optionalen visuellen Inhalt */}
</section>

Wie Hero.astro flexible Varianten ermöglicht

Beachte den variant-Prop in Hero.astro. Statt zwei separate Komponenten (HeroCentered und HeroLeft) zu bauen, nutzt eine einzige Komponente class:list, um das Layout zu variieren. Das CSS im <style>-Block enthält dann .hero--centered und .hero--left Modifier. Ein Organismus, zwei Erscheinungsbilder, eine einzige Wartungsstelle.

Die eine Regel: Abhängigkeiten fließen nur nach unten

Wenn es eine einzige Regel gibt, die du aus Atomic Design mitnimmst, dann diese: Importiere niemals nach oben. Atome importieren nichts. Moleküle importieren nur Atome. Organismen importieren Moleküle (und selten Atome). Layouts komponieren Organismen. Seiten nutzen Layouts.

Was passiert, wenn du diese Regel brichst? Hier ein Vergleich:

Aspekt Korrekte Richtung Recommended Regel gebrochen
Import-Richtung Molekül importiert Atom Atom importiert Molekül
Abhängigkeiten Klar und linear Zirkulär und unvorhersagbar
Einzelne Änderung Wirkt nach oben — kontrolliert Wirkt in alle Richtungen — Chaos
Build-Fehler Sofort lokalisierbar Kaskadenfehler über Ebenen
Testbarkeit Atom allein testbar Atom braucht Molekül braucht Organismus
Refactoring Austausch auf einer Ebene Alles muss gleichzeitig geändert werden
Neuer Entwickler Versteht Hierarchie sofort Muss ganzes System verstehen

Ein konkretes Beispiel

Stell dir vor, dein Button.astro-Atom importiert Card.astro (ein Molekül), weil der Button eine Card-Referenz als Tooltip zeigen soll. Jetzt hängt Button von Card ab, aber Card importiert bereits Button. Zirkuläre Abhängigkeit. Astro kompiliert möglicherweise noch, aber du hast ein fragiles System geschaffen, in dem eine Änderung an Card den Button brechen kann — und damit jede andere Komponente, die Button nutzt.

Die Lösung: Der Tooltip wird ein eigenes Molekül (Tooltip.astro), das Button und Text importiert. Die Karte nutzt Tooltip, nicht umgekehrt.

Design Tokens als Bindeglied

Design Tokens sind das unsichtbare Bindeglied zwischen allen Ebenen. Eine Farbänderung in :root fließt automatisch durch jedes Atom, Molekül und Organismus. Kein manuelles Update von 45 Komponenten.

/* src/styles/tokens/colors.css */
:root {
  /* Primärfarbe — eine Änderung hier aktualisiert das gesamte Projekt */
  --color-primary-500: #6366f1;
  --color-primary-600: #4f46e5;
  --color-primary-700: #4338ca;

  /* Semantische Tokens — mapped auf Basis-Tokens */
  --color-link: var(--color-primary-500);
  --color-focus-ring: var(--color-primary-500);
  --color-btn-primary-bg: var(--color-primary-600);
  --color-btn-primary-hover: var(--color-primary-700);
}

/* Dark Mode — nur semantische Werte überschreiben */
[data-theme="dark"] {
  --color-primary-500: #818cf8;
  --color-btn-primary-bg: var(--color-primary-500);
}
/* Button.astro <style> — Atom nutzt Token */
.btn--primary {
  background-color: var(--color-btn-primary-bg);
  color: var(--color-text-on-primary);
}

.btn--primary:hover {
  background-color: var(--color-btn-primary-hover);
}

.btn:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}
/* Card.astro <style> — Molekül nutzt gleiche Token */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
}

.card:hover {
  border-color: var(--color-primary-500);
  box-shadow: var(--shadow-md);
}
/* Hero.astro <style> — Organismus nutzt gleiche Token */
.hero {
  padding: var(--space-section-lg) var(--space-4);
  background: var(--color-bg);
}

.hero__subtitle {
  color: var(--color-text-secondary);
  max-width: 60ch;
}

.hero__actions {
  display: flex;
  gap: var(--space-4);
  margin-top: var(--space-8);
}

Kompositionsmuster in Astro

Astro bietet vier besonders nützliche Muster für Atomic Design, die ich in diesem Projekt durchgehend einsetze. Jedes Muster löst ein spezifisches Problem der Komponentenkomposition.

Die 4 Kompositionsmuster

  1. class:list für dynamische Klassen

    Astros class:list akzeptiert Arrays, Objekte und Strings. Ideal für Varianten: class:list={['btn', `btn--${variant}`, { 'btn--disabled': disabled }]}. Kein classnames-Paket nötig — Astro kann das nativ. Nutze es in jedem Atom für bedingte Modifikatoren.

  2. <slot /> für flexible Inhalte

    Slots sind Astros Content-Injection-Mechanismus. Statt Inhalte als Props zu übergeben (error-prone bei HTML), nutze <slot /> für alles, was mehr als ein einfacher String ist. Named Slots (<slot name="header" />) ermöglichen mehrere Injection-Punkte. Das Hero-Atom nutzt einen Default-Slot für optionalen visuellen Content.

  3. TypeScript Props Interface

    Jede Komponente definiert ein interface Props {} im Frontmatter. Das bietet Autovervollständigung in der IDE, Compile-Zeit-Fehler bei falschen Props, und eine lebendige Dokumentation. Optionale Props mit Defaults: const { variant = 'primary' } = Astro.props; — so ist der Default explizit und der Typ automatisch korrekt.

  4. Bedingtes Tag-Rendering

    Button.astro rendert <a> oder <button> basierend auf dem href-Prop. Das HTML-Element wird zur Build-Zeit entschieden: const Tag = href ? 'a' : 'button'. Dann: <Tag class:list={classes} {...attrs}>. Das sichert semantisches HTML, ohne zwei separate Komponenten zu brauchen.

Warum Slots besser als Props für Layout sind

Ein häufiger Fehler bei Design-Systemen: Alles über Props lösen wollen. Stell dir eine InfoBox vor, die einen Titel, Beschreibung und eine Liste von Links akzeptieren soll. Mit Props:

<InfoBox title="Hinweis" description="Beachte folgende Punkte:" links={[...]} />

Das funktioniert — bis jemand einen Link fett machen will oder ein Icon einfügen möchte. Mit Slots:

<InfoBox variant="tip" title="Hinweis">
  Beachte folgende Punkte:

  - **Erster Punkt** mit [einem Link](/)
  - Zweiter Punkt mit einem <Icon name="check" size={16} /> Icon
</InfoBox>

Slots sind flexibler, weil der Inhalt beliebiges Markup enthalten kann. Props sind besser für strukturierte Daten (Titel, Variant, Size). Die Faustregel: Props für Konfiguration, Slots für Content.

Lektionen aus der Praxis

Nach Monaten Arbeit mit Atomic Design in einem echten Produktionsprojekt — hier die wichtigsten Erkenntnisse, die kein Theorie-Artikel dir sagen wird.

Starte mit Atomen

Baue die Atome zuerst. Widerstehe der Versuchung, mit der Hero-Sektion zu beginnen. Wenn deine Atome solide sind, bauen sich Moleküle und Organismen fast von selbst.

Nicht über-abstrahieren

Nicht jedes <div> muss ein Atom sein. Wenn etwas nur an einer einzigen Stelle genutzt wird und keine Props braucht, braucht es keine eigene Komponente. Abstrahiere erst beim zweiten Vorkommen.

Slots statt Props für Layout

Props für Konfiguration (variant, size, disabled). Slots für Inhalte (Text, verschachtelte Komponenten, Markup). Slots sind flexibler und brechen seltener.

Eine Komponente, ein Job

Wenn eine Komponente zwei verschiedene Dinge tut, teile sie auf. FormField validiert nicht — es zeigt nur den Fehlerstatus an. Die Validierungslogik lebt in einer separaten Utility.

Design Tokens als Fundament

Definiere Tokens bevor du die erste Komponente baust. Farben, Abstände, Schriftgrößen, Radien, Schatten. Alles in :root. Jede Komponente referenziert nur Tokens, nie rohe Werte.

Blog-Unterordner für Kontext

Kontextspezifische Komponenten (Blog-InfoBox, Blog-CodeTabs) gehören in einen Unterordner. Das verhindert Namenskonflikte und signalisiert klar: Diese Komponente macht nur im Blog-Kontext Sinn.

Die Zwei-Vorkommen-Regel

Eine Regel, die mir Hunderte Stunden gespart hat: Erstelle keine Abstraktion beim ersten Vorkommen. Wenn du einen bestimmten Button-Stil brauchst, hardcode ihn beim ersten Mal. Wenn du ihn ein zweites Mal brauchst, extrahiere das Atom. Warum? Beim ersten Mal weißt du noch nicht, welche Props du brauchen wirst. Beim zweiten Mal ist das Muster klar.

Ausnahme: Grundlegende Atome wie Button, Heading, Text, Icon — diese weißt du von Anfang an, dass du sie überall brauchen wirst. Starte damit.

Atome und Design Tokens definiert

Button, Heading, Text, Icon, Input, Badge — die Grundbausteine. Parallel dazu tokens/colors.css, typography.css, spacing.css. Kein einziger hartcodierter Wert von Anfang an.

Moleküle zusammengesetzt

Card, NavLink, FormField, Breadcrumbs, SearchBar — aus den vorhandenen Atomen komponiert. Hier wurde die Stabilität der Atome auf die Probe gestellt. Einige Props mussten erweitert werden.

Organismen und Layouts gebaut

Header, Footer, Hero, Contact, Portfolio — die großen Sektionen. BaseLayout als Rahmen für alles. Hier zahlte sich die Vorarbeit bei den Atomen aus: Das Zusammenbauen ging schnell.

Blog-System und MDX-Komponenten

InfoBox, CodeTabs, ComparisonTable, FaqAccordion — Blog-spezifische Komponenten im blog/-Unterordner. Content Collections für dreisprachige Posts. 106 Seiten generiert.

Iterieren und Refactoren

Neue Anforderungen deckten fehlende Props auf. Das Tag-Atom entstand, als ich es zum dritten Mal brauchte. Der LanguageSwitcher wurde vom Atom zum Molekül, als er Icon und Link importieren musste.

FAQ — Häufige Fragen zur Praxis

Frequently Asked Questions

Fazit

Atomic Design ist keine akademische Übung — es ist ein konkretes System, das reale Probleme löst. Dieses Portfolio mit seinen 45+ Komponenten, 3 Layouts und 106 Seiten wäre ohne die klare Hierarchie ein unwartbares Durcheinander. Die wichtigsten Takeaways:

  1. Starte mit Atomen und Tokens — sie sind das Fundament, auf dem alles aufbaut
  2. Respektiere die Richtungsregel — Abhängigkeiten fließen nur nach unten
  3. Nutze Slots für Content, Props für Konfiguration — das macht Komponenten flexibel, ohne fragil zu werden
  4. Abstrahiere beim zweiten Vorkommen — nicht beim ersten, nicht beim dritten
  5. Organisiere kontextspezifische Komponenten in Unterordnerblog/ hält die Hauptordner sauber

Das Ergebnis: Ein System, bei dem ich eine Farbe in einer Token-Datei ändere und das gesamte Projekt konsistent aktualisiert wird. Ein System, bei dem neue Seiten in Minuten statt Stunden zusammengebaut werden, weil die Bausteine bereits existieren und zusammenpassen.