11 KiB
Design-Dokument – System-Theme-Erkennung (Light/Dark Mode)
Übersicht
Dieses Feature integriert die bereits installierte Bibliothek next-themes (v0.3.0) in die bestehende React + Vite + Tailwind + shadcn/ui-Webseite, um eine automatische Erkennung des Betriebssystem-Themes (hell/dunkel) sowie manuelles Umschalten zu ermöglichen.
Aktuell definiert :root in src/index.css das dunkle Farbschema. Es existiert zwar ein .dark-Block, dieser wird aber nicht aktiv genutzt. Die Tailwind-Konfiguration verwendet bereits darkMode: ["class"], was perfekt mit next-themes zusammenarbeitet.
Kernentscheidungen:
next-themeswird als ThemeProvider eingesetzt (bereits als Dependency vorhanden,attribute: "class"für Tailwind-Kompatibilität)- Das aktuelle
:root-Farbschema wird zum Light Theme umstrukturiert, ein neues Dark Theme wird unter.darkdefiniert - Ein Theme-Toggle-Button wird im Header platziert (Desktop und Mobile)
- Ein Inline-Script in
index.htmlverhindert Theme-Flackern beim Laden (FOUC-Prevention)
Architektur
graph TD
A[index.html – Inline-Script] -->|Setzt class auf html| B[html-Element]
B --> C[ThemeProvider – next-themes]
C --> D[App.tsx]
D --> E[Header mit ThemeToggle]
D --> F[Alle Seiten & Komponenten]
G[localStorage – theme] <-->|Lesen/Schreiben| C
H[prefers-color-scheme] -->|System-Erkennung| C
I[index.css – :root Light] --> F
J[index.css – .dark Dark] --> F
Datenfluss
- Beim Laden der Seite liest ein Inline-Script in
index.htmldenlocalStorage-Wert (theme) aus. Falls keiner vorhanden ist, wirdprefers-color-schemegeprüft. Die entsprechende CSS-Klasse (darkoder keine) wird auf<html>gesetzt – noch bevor React rendert. ThemeProviderausnext-themesübernimmt die Verwaltung im React-Baum. Er synchronisiert den Zustand mitlocalStorageund reagiert auf Änderungen der System-Einstellung viamatchMedia-Listener.- Der
ThemeToggle-Button im Header nutzt denuseTheme()-Hook, um zwischenlight,darkundsystemzu wechseln. - Alle Komponenten nutzen weiterhin
hsl(var(--...))CSS-Variablen – der Wechsel erfolgt rein über CSS-Klassen.
Komponenten und Schnittstellen
1. ThemeProvider-Wrapper (src/components/ThemeProvider.tsx)
Dünner Wrapper um ThemeProvider aus next-themes:
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: string; // Standard: "system"
storageKey?: string; // Standard: "theme"
enableSystem?: boolean; // Standard: true
disableTransitionOnChange?: boolean; // Standard: false
}
Konfiguration:
attribute="class"– Tailwind-kompatibeldefaultTheme="system"– Erstbesucher erhalten System-ThemeenableSystem={true}– Automatische Erkennung aktivstorageKey="theme"– localStorage-Schlüssel
Wird in App.tsx als äußerster Wrapper um den gesamten Komponentenbaum eingebunden.
2. ThemeToggle-Komponente (src/components/ThemeToggle.tsx)
Button-Komponente mit Dropdown-Menü (shadcn/ui DropdownMenu):
interface ThemeToggleProps {
className?: string;
}
Funktionalität:
- Zeigt ein Icon basierend auf dem aktuellen Theme (Sun, Moon, Monitor aus
lucide-react) - Dropdown mit drei Optionen: „Hell", „Dunkel", „System"
- Nutzt
useTheme()Hook fürtheme,setTheme,resolvedTheme aria-labelbeschreibt den aktuellen Zustand- Tastatur-bedienbar (Tab, Enter/Space)
- Mounted-Check verhindert Hydration-Mismatch (Icon wird erst nach Mount gerendert)
3. Anpassungen am Header (src/components/Header.tsx)
- ThemeToggle wird im Desktop-NavBody neben dem „Kontakt"-Button eingefügt
- ThemeToggle wird im Mobile-Nav-Header neben dem Hamburger-Menü eingefügt
- Positionierung: rechts, vor dem Kontakt-Button (Desktop) bzw. links vom Toggle-Icon (Mobile)
4. Inline-Script in index.html
Ein <script>-Block im <head>, der vor dem React-Bundle ausgeführt wird:
// Liest localStorage("theme") aus
// Falls "system" oder nicht vorhanden: prüft prefers-color-scheme
// Setzt class="dark" auf <html> wenn Dark Mode aktiv
Dieses Script verhindert das sichtbare Flackern (FOUC) beim Seitenaufruf.
5. CSS-Variablen-Umstrukturierung (src/index.css)
:root→ Light Theme (helle Hintergründe, dunkle Texte).dark→ Dark Theme (das aktuelle:root-Farbschema wird hierhin verschoben)- Alle bestehenden Komponentenstile (
.glass-nav,.card-minimal,.project-card,.btn, etc.) werden auf Theme-Kompatibilität geprüft und ggf. angepasst - Hardcodierte Farbwerte in Komponentenstilen werden durch CSS-Variablen ersetzt
Komponentendiagramm
graph LR
subgraph App.tsx
TP[ThemeProvider]
TP --> Router
end
subgraph Header
TT[ThemeToggle]
TT -->|useTheme| TP
end
subgraph CSS
LT[":root – Light Theme"]
DT[".dark – Dark Theme"]
end
TP -->|class auf html| CSS
Datenmodelle
Theme-Zustand
type ThemeValue = "light" | "dark" | "system";
// Von next-themes bereitgestellt via useTheme()
interface ThemeState {
theme: ThemeValue; // Gewählte Präferenz ("light" | "dark" | "system")
resolvedTheme: "light" | "dark"; // Tatsächlich angewendetes Theme
setTheme: (theme: ThemeValue) => void;
systemTheme: "light" | "dark"; // Aktuelle OS-Einstellung
}
localStorage-Schema
| Schlüssel | Wert | Beschreibung |
|---|---|---|
theme |
"light" | "dark" | "system" |
Gespeicherte Benutzerpräferenz |
CSS-Custom-Properties (Light Theme – neu zu erstellen)
:root {
--background: 0 0% 100%; /* Weiß */
--foreground: 0 0% 9%; /* Fast Schwarz */
--card: 0 0% 98%; /* Sehr helles Grau */
--card-foreground: 0 0% 9%;
--primary: 198 93% 42%; /* Cyan-Blau (konsistent) */
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 94%; /* Helles Grau */
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 94%;
--muted-foreground: 0 0% 40%;
--accent: 0 0% 94%;
--accent-foreground: 0 0% 9%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 198 93% 42%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 98%;
/* ... weitere Token analog */
}
CSS-Custom-Properties (Dark Theme – bestehendes Schema)
.dark {
--background: 0 0% 0%; /* Schwarz (aktuelles :root) */
--foreground: 0 0% 92%;
--card: 0 0% 6%;
--card-foreground: 0 0% 92%;
--primary: 180 40% 50%;
--primary-foreground: 0 0% 3%;
/* ... restliche aktuelle :root-Werte */
}
Korrektheitseigenschaften (Correctness Properties)
Eine Korrektheitseigenschaft ist ein Merkmal oder Verhalten, das über alle gültigen Ausführungen eines Systems hinweg gelten soll – im Wesentlichen eine formale Aussage darüber, was das System tun soll. Eigenschaften bilden die Brücke zwischen menschenlesbaren Spezifikationen und maschinell überprüfbaren Korrektheitsgarantien.
Property 1: System-Theme-Auflösung
Für jede beliebige System-Theme-Einstellung (hell oder dunkel) gilt: Wenn die Theme-Präferenz auf „system" steht (oder keine Präferenz gespeichert ist), dann soll das aufgelöste Theme (resolvedTheme) dem aktuellen System-Theme entsprechen.
Validates: Requirements 1.1, 1.2, 1.3, 2.5
Property 2: Explizite Theme-Wahl überschreibt System
Für jede beliebige System-Theme-Einstellung und für jede explizite Theme-Wahl ("light" oder "dark") gilt: Das aufgelöste Theme (resolvedTheme) soll immer der expliziten Wahl entsprechen, unabhängig vom System-Theme.
Validates: Requirements 2.3, 2.4
Property 3: Persistenz-Round-Trip
Für jeden gültigen Theme-Wert ("light", "dark", "system") gilt: Wenn der Wert über setTheme() gesetzt wird, dann soll der im localStorage gespeicherte Wert identisch sein, und beim erneuten Laden soll dieser gespeicherte Wert korrekt als aktive Präferenz wiederhergestellt werden.
Validates: Requirements 3.1, 3.2
Fehlerbehandlung
| Szenario | Verhalten |
|---|---|
localStorage nicht verfügbar (blockiert, privater Modus) |
ThemeProvider fällt auf System-Theme-Erkennung zurück. Kein Fehler wird geworfen. Die Seite funktioniert normal, nur die Persistenz entfällt. |
prefers-color-scheme nicht unterstützt (ältere Browser) |
next-themes fällt auf den defaultTheme zurück ("system" → wird als "light" aufgelöst). |
Ungültiger Wert im localStorage |
next-themes ignoriert ungültige Werte und fällt auf defaultTheme zurück. |
| JavaScript deaktiviert | Das Inline-Script in index.html wird nicht ausgeführt. Die Seite zeigt das Standard-CSS (Light Theme in :root). Kein Theme-Toggle verfügbar, aber die Seite bleibt nutzbar. |
| Hydration-Mismatch (SSR/SSG) | Nicht relevant für Vite-SPA. Der Mounted-Check im ThemeToggle verhindert trotzdem Icon-Flackern. |
Teststrategie
Property-Based Tests (fast-check)
Bibliothek: fast-check (für TypeScript/Vitest)
Konfiguration:
- Minimum 100 Iterationen pro Property-Test
- Jeder Test referenziert die zugehörige Design-Property
- Tag-Format: Feature: system-theme-detection, Property {number}: {property_text}
Die drei Korrektheitseigenschaften werden als Property-Based Tests implementiert:
- Property 1 – Theme-Auflösungslogik bei „system"-Präferenz: Generiere zufällige System-Theme-Werte, setze Präferenz auf „system", prüfe
resolvedTheme === systemTheme. - Property 2 – Explizite Wahl überschreibt System: Generiere zufällige Kombinationen aus System-Theme und expliziter Wahl, prüfe
resolvedTheme === expliziteWahl. - Property 3 – Persistenz-Round-Trip: Generiere zufällige gültige Theme-Werte, speichere via
setTheme(), lese auslocalStorage, prüfe Gleichheit. Simuliere Neustart, prüfe ob Wert wiederhergestellt wird.
Unit Tests (Vitest + React Testing Library)
Ergänzend zu den Property-Tests werden folgende Example-basierte Tests geschrieben:
- ThemeToggle-Rendering: Button ist im Header vorhanden (Req. 2.1)
- Dropdown-Optionen: Drei Optionen (Hell, Dunkel, System) sind vorhanden (Req. 2.2)
- Icon-Zuordnung: Korrektes Icon für jeden Theme-Zustand (Req. 2.6)
- CSS-Token-Vollständigkeit: Alle erforderlichen Custom Properties sind im Light Theme definiert (Req. 4.1)
- Kontrastverhältnisse: Schlüssel-Farbpaare erfüllen WCAG-AA (Req. 4.2)
- Tastatur-Bedienbarkeit: Tab-Navigation und Enter/Space funktionieren (Req. 6.1)
- aria-label: Beschreibt den aktuellen Zustand korrekt (Req. 6.2)
- aria-live: Zustandswechsel wird kommuniziert (Req. 6.3)
Edge-Case Tests
- localStorage blockiert: Fallback auf System-Theme ohne Fehler (Req. 3.3)
Integrationstests
- matchMedia-Listener: Simuliere OS-Theme-Wechsel, prüfe ob resolvedTheme sich aktualisiert (Req. 1.4)
Nicht automatisiert testbar
- Visuell korrekte Darstellung der Komponentenstile im Light Theme (Req. 4.3) → Manuelle visuelle Prüfung
- Kein sichtbares Flackern beim Laden (Req. 5.2) → Manuelle visuelle Prüfung