von Design-Tokens bis zum persistierten Override
Dark Mode ist längst keine Kür mehr, sondern eine Nutzererwartung. Wer Farben in React Native direkt in StyleSheet.create hardcodiert, baut sich technische Schulden auf, sobald Dark Mode oder ein zweites Farbschema gefordert wird. Design-Tokens, ein zentraler ThemeProvider und systematisches Theming lösen dieses Problem einmalig und sauber.
Inhaltsverzeichnis
- 1. Warum hardcodierte Farben ein Theming-Problem sind
- 2. Design-Tokens als Single Source of Truth
- 3. useColorScheme: die OS-Einstellung erkennen
- 4. Der ThemeProvider mit React Context
- 5. Manueller Override: Light, Dark, System
- 6. Persistenz mit AsyncStorage
- 7. Bilder, Icons und Status Bar pro Theme
- 8. Flicker vermeiden und Style-Caches testen
- 9. Theming-Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum hardcodierte Farben ein Theming-Problem sind
In vielen React Native Codebasen sieht man Farbwerte wie backgroundColor: '#ffffff' direkt in Dutzenden Komponenten verteilt. Solange nur ein einziges, statisches Farbschema existiert, funktioniert das. Sobald Dark Mode und Theming gefordert werden, muss aber jede einzelne Stelle manuell gefunden und angepasst werden, was in größeren Apps schnell hunderte Fundstellen bedeutet und garantiert Inkonsistenzen produziert.
Das eigentliche Problem ist nicht Dark Mode selbst, sondern fehlende Indirektion. Eine Komponente sollte nie wissen, ob "Weiß" oder "Dunkelgrau" gemeint ist, sondern nur, dass sie die Hintergrundfarbe für eine Karte benötigt. Systematisches Dark Mode und Theming führt genau diese Indirektionsebene ein, sodass eine einzige Änderung an einem Token alle betroffenen Komponenten gleichzeitig aktualisiert.
Der Aufwand, Dark Mode und Theming nachträglich in eine bestehende App einzuführen, steigt mit jeder Woche, in der neue Komponenten weiterhin hardcodierte Farben verwenden. Ein früher Architekturentscheid für Design-Tokens zahlt sich deshalb über die gesamte Projektlaufzeit aus, nicht nur beim ersten Dark-Mode-Release.
2. Design-Tokens als Single Source of Truth
Design-Tokens sind benannte Werte für Farbe, Abstand und Typografie, die unabhängig von einer konkreten Plattform oder einem konkreten Theme definiert sind. Statt #1e1b4b direkt zu verwenden, referenziert Code einen semantischen Token wie background.primary, der je nach aktivem Theme unterschiedliche konkrete Werte annimmt. Dieses Muster ist das Fundament jedes robusten Dark Mode und Theming-Systems.
Wichtig ist die semantische statt beschreibende Benennung: text.primary statt gray900, weil sich im Dark Mode die konkrete Farbe hinter dem Token ändert, der Name aber stabil bleibt. Eine Komponente, die colors.text.primary verwendet, muss beim Themewechsel nicht angepasst werden, weil die Zuordnung ausschließlich im Token-File liegt.
{
"light": {
"background": { "primary": "#ffffff", "secondary": "#f1f5f9" },
"text": { "primary": "#0f172a", "secondary": "#475569" },
"accent": { "default": "#4338ca" }
},
"dark": {
"background": { "primary": "#0f172a", "secondary": "#1e293b" },
"text": { "primary": "#f1f5f9", "secondary": "#94a3b8" },
"accent": { "default": "#818cf8" }
}
}
3. useColorScheme: die OS-Einstellung erkennen
Der React Native Hook useColorScheme() liefert den aktuell aktiven System-Modus, entweder 'light', 'dark' oder null, falls das Betriebssystem keine Präferenz meldet. Er reagiert live auf Änderungen: Wechselt der Nutzer im Betriebssystem zwischen Light und Dark Mode, während die App im Hintergrund läuft, aktualisiert sich der Wert automatisch beim nächsten Vordergrundwechsel.
Für ein sauberes Dark Mode und Theming-System reicht useColorScheme() allein nicht aus, weil viele Apps dem Nutzer zusätzlich eine manuelle Override-Option anbieten wollen, unabhängig von der Systemeinstellung. Der Hook liefert aber die Grundlage, auf der ein eigener Theme-Zustand aufgebaut wird, der System-Präferenz und manuellen Override kombiniert.
4. Der ThemeProvider mit React Context
Ein ThemeProvider stellt Design-Tokens und den aktuellen Theme-Namen über React Context der gesamten Komponenten-Hierarchie zur Verfügung. Jede Komponente greift über einen useTheme()-Hook auf die aktuellen Token-Werte zu, statt eigene Logik zur Theme-Ermittlung zu implementieren. Diese Zentralisierung ist der Kern jedes wartbaren Dark Mode und Theming-Ansatzes.
Wichtig ist, dass der Context-Provider so hoch wie möglich im Komponentenbaum sitzt, meist direkt unterhalb des Root-Elements, damit auch Navigations-Header und Modals konsistent auf dieselben Tokens zugreifen können. Ein Context-Wert, der sich ändert, löst automatisch ein Re-Rendering aller konsumierenden Komponenten aus, was Theme-Wechsel ohne manuellen Eingriff propagiert.
// theme/ThemeProvider.tsx — centralizes theme state and design tokens
import React, { createContext, useContext, useMemo, useState, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import tokens from './tokens.json';
type ThemeMode = 'light' | 'dark' | 'system';
type ThemeContextValue = {
colors: typeof tokens.light;
mode: ThemeMode;
setMode: (mode: ThemeMode) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const systemScheme = useColorScheme();
const [mode, setMode] = useState<ThemeMode>('system');
const resolvedScheme = mode === 'system' ? (systemScheme ?? 'light') : mode;
const colors = tokens[resolvedScheme];
const value = useMemo(() => ({ colors, mode, setMode }), [colors, mode]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider');
return ctx;
}
5. Manueller Override: Light, Dark, System
Nutzer erwarten heute meist drei Optionen: dem System folgen, immer Light Mode, immer Dark Mode. Der dritte Zustand "System" ist kein zusätzliches Theme, sondern eine Anweisung, weiterhin useColorScheme() zu befolgen, statt eines festen Werts. Diese Unterscheidung sauber im State-Modell abzubilden verhindert, dass Dark Mode und Theming-Logik unnötig verzweigt.
Die Einstellungen-UI selbst ist meist ein einfaches Segment-Control mit drei Optionen, das den setMode()-Setter aus dem ThemeProvider aufruft. Wichtig ist, dass diese Einstellung nicht nur im Speicher, sondern dauerhaft persistiert wird, damit der gewählte Modus nach einem App-Neustart erhalten bleibt.
6. Persistenz mit AsyncStorage
Ohne Persistenz würde jeder App-Start den Theme-Modus auf "System" zurücksetzen, selbst wenn ein Nutzer explizit Dark Mode erzwungen hat. AsyncStorage speichert den gewählten Modus als einfachen String und liest ihn beim App-Start, noch bevor die erste Komponente rendert, um ein kurzes Aufblitzen des falschen Themes zu vermeiden.
Der Ladevorgang aus AsyncStorage ist asynchron, was bedeutet, dass die App kurzzeitig mit einem Default-Theme startet, bevor der gespeicherte Wert vorliegt. Für ein flicker-freies Dark Mode und Theming-Erlebnis blendet man in dieser kurzen Ladephase meist einen neutralen Splash-Screen ein, statt das UI mit dem falschen Theme zu zeigen.
#!/usr/bin/env bash
# Install AsyncStorage for theme mode persistence
npm install @react-native-async-storage/async-storage
cd ios && pod install && cd ..
echo "AsyncStorage installed for theme persistence"
7. Bilder, Icons und Status Bar pro Theme
Nicht nur Farben, auch Bild-Assets müssen theme-abhängig ausgetauscht werden: Ein Logo mit dunkler Schrift auf hellem Hintergrund ist im Dark Mode unlesbar. Die robuste Lösung speichert für kritische Assets zwei Varianten und wählt sie über denselben useTheme()-Hook aus, statt CSS-Filter oder Nachbearbeitung zur Laufzeit zu versuchen.
Die Status Bar muss ebenfalls mit dem Theme synchron bleiben: StatusBar barStyle="light-content" im Dark Mode, "dark-content" im Light Mode. Wird das vergessen, verschwindet die Uhrzeit- und Akku-Anzeige optisch im hellen Hintergrund, ein häufig übersehener Fehler bei nachträglichem Dark Mode und Theming.
// ios/Info.plist — UIUserInterfaceStyle touch point for forcing appearance
// Leaving this key unset lets the app follow useColorScheme() dynamically.
// Setting it to "Dark" or "Light" would force the OS-level appearance
// and should only be used for apps that intentionally opt out of system theming.
8. Flicker vermeiden und Style-Caches testen
Ein häufiges Symptom fehlerhaften Dark Mode und Theming ist ein kurzes Aufblitzen des falschen Themes beim App-Start oder Theme-Wechsel. Ursache ist meist, dass Styles mit StyleSheet.create() einmalig zur Modulinitialisierung berechnet werden, statt bei jedem Render mit den aktuellen Token-Werten neu erzeugt zu werden.
Die Lösung: Style-Objekte, die von Theme-Tokens abhängen, dürfen nicht auf Modulebene mit StyleSheet.create() vordefiniert werden, sondern müssen innerhalb der Komponente mit useMemo() aus den aktuellen Colors berechnet werden. Ein Test, der bewusst zwischen Light und Dark wechselt und einen Screenshot-Vergleich durchführt, deckt solche Caching-Fehler zuverlässig auf.
9. Theming-Ansätze im Vergleich
Es gibt mehrere etablierte Wege, Dark Mode und Theming in React Native umzusetzen, mit unterschiedlichen Kompromissen zwischen Flexibilität, Bundle-Größe und Lernkurve.
| Ansatz | Flexibilität | Bundle-Impact | Wann geeignet |
|---|---|---|---|
| useColorScheme + Context | Sehr hoch, volle Kontrolle | Keine Zusatzabhängigkeit | Standard-Empfehlung für die meisten Apps |
| styled-components ThemeProvider | Hoch, CSS-in-JS-Syntax | Zusätzliche Laufzeitbibliothek | Teams mit Web-Hintergrund und CSS-in-JS-Präferenz |
| NativeWind dark: Variante | Mittel, an Tailwind-Klassen gebunden | Build-Zeit-Transformation, kein Laufzeit-Overhead | Teams, die bereits Tailwind-Utility-Klassen nutzen |
| Shopify Restyle | Hoch, typsicher | Kleine Zusatzbibliothek | Design-System-lastige Apps mit striktem TypeScript |
Für die meisten React Native Projekte ist die Kombination aus useColorScheme() und einem eigenen Context-basierten ThemeProvider der pragmatischste Einstieg in Dark Mode und Theming, ohne zusätzliche Laufzeitabhängigkeit. NativeWind lohnt sich, wenn Tailwind-Klassen bereits Teil des Stacks sind, Restyle punktet bei strikt typisierten Design-Systemen.
Mironsoft
React Native Design-Systeme, Theming und UI-Konsistenz
Dark Mode ohne Flicker und ohne Chaos?
Wir bauen ein systematisches Design-Token-System für eure React Native App, mit ThemeProvider, persistiertem Override und flicker-freiem Theme-Wechsel für iOS und Android.
Design-Tokens
Semantische Farb-, Abstands- und Typografie-Tokens als Single Source of Truth
ThemeProvider-Setup
Context-basiertes Theming mit System-Erkennung und manuellem Override
Migrations-Support
Bestehende hardcodierte Farben systematisch auf Tokens umstellen
10. Zusammenfassung
Systematisches Dark Mode und Theming in React Native beginnt mit Design-Tokens als Single Source of Truth, nicht mit hardcodierten Farbwerten in einzelnen Komponenten. useColorScheme() erkennt die Systempräferenz, ein Context-basierter ThemeProvider stellt die aktiven Tokens der gesamten App zur Verfügung, und ein manueller Override zwischen Light, Dark und System deckt die Erwartungen der meisten Nutzer ab.
Persistenz mit AsyncStorage sorgt dafür, dass der gewählte Modus einen App-Neustart übersteht, während sorgfältige Behandlung von Bild-Assets, Status Bar und Style-Berechnung Flicker und optische Inkonsistenzen verhindert. Wer diese Bausteine von Anfang an einplant, spart sich ein aufwendiges Nachrüsten, sobald Dark Mode zur Pflichtanforderung wird.
React Native Dark Mode und Theming — Das Wichtigste auf einen Blick
Design-Tokens
Semantische Benennung statt beschreibender Namen, damit Themewechsel keine Code-Änderung erfordern.
useColorScheme
Erkennt die System-Präferenz live, Grundlage für jeden ThemeProvider.
Persistenz
AsyncStorage speichert den manuellen Override über App-Neustarts hinweg.
Flicker-Vermeidung
Styles mit useMemo statt statischem StyleSheet.create bei Theme-abhängigen Werten.