und Design Tokens in Vue Apps
Dark Mode mit einem einfachen Klasse-Toggle zu implementieren ist der erste Schritt. Ein professionelles Theme-System mit Design Tokens, System-Präferenz-Erkennung, Persistenz und flimmerfreiem SSR-Laden ist das, was Nutzern in der Praxis tatsächlich zugutekommt. Vue 3 bietet alle Bausteine – es geht darum, sie richtig zusammenzusetzen.
Inhaltsverzeichnis
- 1. Was Design Tokens sind und warum sie Dark Mode vereinfachen
- 2. CSS Custom Properties als Design-Token-Layer
- 3. Das useTheme-Composable: Dark Mode in Vue 3 kapseln
- 4. System-Präferenz erkennen: prefers-color-scheme und matchMedia
- 5. Theme-Persistenz: LocalStorage und Pinia-Integration
- 6. Tailwind CSS Dark Mode: class vs. media Strategy
- 7. Mehrere Themes: Beyond Dark und Light
- 8. Das Flash-of-Wrong-Theme-Problem bei SSR lösen
- 9. Theme-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Design Tokens sind und warum sie Dark Mode vereinfachen
Design Tokens sind benannte, semantische Designentscheidungen in Form von Variablen: --color-surface statt #ffffff, --color-text-primary statt #111827, --color-border statt #e5e7eb. Der entscheidende Unterschied zu einfachen Farbwerten: Design Tokens beschreiben die Rolle einer Farbe, nicht ihren visuellen Wert. --color-surface ist im Light Mode weiß und im Dark Mode dunkelgrau – der Name bleibt gleich, der Wert ändert sich je nach Theme.
Ohne Design Tokens sieht Dark-Mode-Implementierung so aus: Jede Komponente hat explizite Dark-Mode-Klassen wie dark:bg-gray-900 dark:text-white. Das funktioniert, skaliert aber schlecht. Bei hundert Komponenten bedeutet eine Theme-Änderung hundert manuelle Anpassungen. Mit Design Tokens als CSS Custom Properties gibt es eine einzige Stelle, an der das Dark-Mode-Theme definiert wird. Alle Komponenten, die --color-surface verwenden, bekommen automatisch den Dark-Mode-Wert. Das ist der fundamentale Vorteil des Token-basierten Ansatzes.
Im Kontext von Vue 3 und Dark Mode sind Design Tokens als CSS Custom Properties implementiert, die auf dem :root-Element oder auf dem html-Element mit einer data-theme- oder class-Selektion überschrieben werden. Das ermöglicht, das Theme durch ein einfaches DOM-Attribut zu wechseln, ohne JavaScript neu zu rendern. Vue reaktiviert nur den State, der das Attribut kontrolliert – der CSS-Wechsel passiert nativ im Browser.
2. CSS Custom Properties als Design-Token-Layer
Die Implementierung von Design Tokens mit CSS Custom Properties beginnt mit einer semantischen Token-Hierarchie. Auf der untersten Ebene liegen primitive Tokens: konkrete Farbwerte wie --green-600: #16a34a. Auf der nächsten Ebene semantische Tokens: --color-primary: var(--green-600). Auf der obersten Ebene komponentenspezifische Tokens: --button-background: var(--color-primary). Diese Hierarchie ermöglicht, das Theme auf der semantischen Ebene zu wechseln, ohne alle primitiven Tokens zu duplizieren.
Das Dark Mode-Theme überschreibt nur die semantischen Tokens. Im :root werden die Light-Mode-Werte definiert. Im [data-theme="dark"] oder .dark-Selektor werden nur die semantischen Tokens überschrieben. Primitive Tokens bleiben unverändert, weil sie keine Bedeutung im Dark-Mode-Kontext haben. Diese Struktur macht Design Token-Verwaltung skalierbar: Neue Farben werden einmal als primitive Tokens definiert und dann in semantische Tokens eingehängt.
/* src/styles/tokens.css */
/* Design Token layer — primitives and semantic tokens */
:root {
/* Primitive tokens (never use directly in components) */
--green-50: #f0fdf4;
--green-600: #16a34a;
--green-700: #15803d;
--slate-50: #f8fafc;
--slate-900: #0f172a;
--white: #ffffff;
/* Semantic tokens — Light Mode defaults */
--color-bg: var(--white);
--color-surface: var(--slate-50);
--color-text-primary: var(--slate-900);
--color-text-muted: #64748b;
--color-border: #e2e8f0;
--color-primary: var(--green-600);
--color-primary-hover: var(--green-700);
/* Component tokens */
--card-bg: var(--color-bg);
--card-border: var(--color-border);
--nav-bg: var(--color-surface);
}
/* Dark Mode — only override semantic tokens */
[data-theme="dark"] {
--color-bg: #0f172a;
--color-surface: #1e293b;
--color-text-primary: #f1f5f9;
--color-text-muted: #94a3b8;
--color-border: #334155;
/* --color-primary stays green — same brand color in both themes */
}
3. Das useTheme-Composable: Dark Mode in Vue 3 kapseln
Das useTheme-Composable ist das Herzstück des Vue-Dark-Mode-Systems. Es kapselt den gesamten Theme-Lifecycle: System-Präferenz lesen, LocalStorage prüfen, aktives Theme setzen, das data-theme-Attribut auf dem html-Element aktualisieren und auf System-Präferenz-Änderungen reagieren. Komponenten, die das Theme anzeigen oder umschalten wollen, rufen einfach useTheme() auf und bekommen theme, isDark und toggleTheme zurück.
Der DOM-Schreibvorgang – document.documentElement.setAttribute('data-theme', theme.value) – sollte in einem watchEffect passieren, der auf den reaktiven Theme-State reagiert. Das entkoppelt den Vue-State von der DOM-Manipulation: Der Theme-State wird reaktiv verwaltet, der Watch-Effekt synchronisiert den State in den DOM. Das ist sauberer als direktes DOM-Schreiben in Methoden, weil der Watch-Effekt auch beim initialen Composable-Aufruf ausgeführt wird und so das initiale Theme korrekt setzt.
Für den globalen Einsatz – alle Komponenten sollen auf dasselbe Theme zugreifen – wird das Composable entweder als Composable Store (Modulscope-State) oder als Pinia-Store implementiert. Bei SSR-Anwendungen ist der Pinia-Store vorzuziehen. Bei Client-only-Anwendungen reicht der Composable-Store-Ansatz mit Modulscope-State, der das aktive Theme als ref außerhalb der Composable-Funktion hält.
// src/composables/useTheme.ts
import { ref, computed, watchEffect, onMounted } from 'vue'
type Theme = 'light' | 'dark' | 'system'
type ResolvedTheme = 'light' | 'dark'
// Module-scope state — shared across all component instances (client-only)
const activeTheme = ref<Theme>('system')
export function useTheme() {
const resolvedTheme = computed<ResolvedTheme>(() => {
if (activeTheme.value === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return activeTheme.value
})
const isDark = computed(() => resolvedTheme.value === 'dark')
// Sync reactive state to DOM attribute + persist to localStorage
watchEffect(() => {
if (typeof document === 'undefined') return // SSR guard
document.documentElement.setAttribute('data-theme', resolvedTheme.value)
localStorage.setItem('theme', activeTheme.value)
})
// Read persisted preference after mount (not during SSR)
onMounted(() => {
const persisted = localStorage.getItem('theme') as Theme | null
if (persisted) activeTheme.value = persisted
})
function setTheme(theme: Theme) { activeTheme.value = theme }
function toggleTheme() {
activeTheme.value = isDark.value ? 'light' : 'dark'
}
return { theme: activeTheme, resolvedTheme, isDark, setTheme, toggleTheme }
}
4. System-Präferenz erkennen: prefers-color-scheme und matchMedia
Das Media-Query-Feature prefers-color-scheme ist der Browser-Standard für System-Dark-Mode-Erkennung. window.matchMedia('(prefers-color-scheme: dark)').matches gibt true zurück, wenn das Betriebssystem oder der Browser Dark Mode aktiviert hat. Das ist der Ausgangspunkt für das Dark Mode-Default: Wenn kein gespeichertes Theme vorhanden ist, richtet sich die Anwendung nach der System-Präferenz. Nutzer, die Dark Mode systemmweit aktiviert haben, bekommen ihn automatisch ohne manuelle Einstellung.
Um auf Änderungen der System-Präferenz zu reagieren – der Nutzer wechselt zwischen Dark und Light Mode im Betriebssystem –, registriert das Composable einen addEventListener auf dem matchMedia-Objekt. Das change-Event wird gefeuert, wenn der Nutzer das System-Theme ändert. Im Composable-Cleanup (onUnmounted) wird der Listener wieder entfernt. Solange das aktive Theme 'system' ist, reagiert die Vue Dark Mode-Implementierung auf diese Änderungen live, ohne Neuladen der Seite.
Ein wichtiger UX-Aspekt: Das 'system'-Theme sollte nicht als separater Wert in einem Toggle-Button erscheinen, sondern als implizites Default-Verhalten. Die UX-Empfehlung: Ein Toggle-Button wechselt zwischen 'light' und 'dark'. Ein separater "Zurücksetzen"-Button setzt das Theme auf 'system'. Das entspricht dem Nutzungsverhalten: Die meisten Nutzer wollen entweder explizit Hell oder Dunkel erzwingen oder das System entscheiden lassen.
5. Theme-Persistenz: LocalStorage und Pinia-Integration
Das gewählte Theme muss über Seitenladevorgänge hinweg persistiert werden. Die einfachste Persistenz-Methode: localStorage.setItem('theme', theme) beim Setzen und localStorage.getItem('theme') beim Initialisieren. Das funktioniert für Client-only-Anwendungen zuverlässig. Die Herausforderung bei SSR: localStorage ist auf dem Server nicht verfügbar, und der initiale HTML-Response kennt das Theme des Nutzers nicht – das führt zum Flash-of-Wrong-Theme-Problem.
Für Pinia-basierte Theme-Persistenz eignet sich das Plugin pinia-plugin-persistedstate: Es serialisiert den Theme-State automatisch in LocalStorage und hydratiert ihn beim Start. In Nuxt können Cookies statt LocalStorage verwendet werden, weil der Server bei jedem Request den Cookie lesen und das Theme server-seitig anwenden kann. Das eliminiert den Theme-Flash vollständig, weil der Server bereits das korrekte data-theme-Attribut im HTML rendert.
6. Tailwind CSS Dark Mode: class vs. media Strategy
Tailwind CSS bietet zwei Strategien für Dark Mode: media und class. Die media-Strategie aktiviert Dark-Mode-Varianten automatisch basierend auf dem System-prefers-color-scheme-Media-Query – ohne JavaScript, ohne DOM-Manipulation. Die class-Strategie aktiviert Dark-Mode-Varianten, wenn eine bestimmte Klasse (dark standardmäßig) auf einem Elternelement vorhanden ist. Für Vue-Anwendungen mit manuellem Theme-Toggle und Persistenz ist class die richtige Wahl, weil sie programmatische Kontrolle über das aktive Theme ermöglicht.
Tailwind CSS v4 erlaubt die Kombination von CSS Custom Properties und Dark-Mode-Varianten besonders elegant. Mit @custom-media --dark (prefers-color-scheme: dark) und Token-Definitionen im CSS kann das Design-Token-System direkt in Tailwind-Klassen genutzt werden. Design Tokens als CSS Custom Properties und Tailwind-Dark-Mode-Klassen sind keine Konkurrenten, sondern Ergänzungen: Tokens für konsistente Basisfarben in Komponenten, Tailwind-Dark-Mode-Klassen für präzise komponentenspezifische Anpassungen.
// tailwind.config.ts — class-based dark mode for programmatic control
import type { Config } from 'tailwindcss'
export default {
// class strategy: dark mode activated by .dark class on html element
darkMode: ['class', '[data-theme="dark"]'],
content: ['./index.html', './src/**/*.{vue,ts,tsx}'],
theme: {
extend: {
// Map design tokens to Tailwind utilities
colors: {
bg: 'var(--color-bg)',
surface: 'var(--color-surface)',
'text-primary': 'var(--color-text-primary)',
'text-muted': 'var(--color-text-muted)',
border: 'var(--color-border)',
primary: 'var(--color-primary)',
},
},
},
} satisfies Config
// Usage in components — semantic token classes instead of hardcoded colors
// bg-bg → var(--color-bg) → white / dark: #0f172a
// text-text-primary → var(--color-text-primary) → slate-900 / dark: #f1f5f9
// dark:bg-surface → also possible for component-specific overrides
7. Mehrere Themes: Beyond Dark und Light
Das Design-Token-System mit CSS Custom Properties skaliert problemlos auf mehr als zwei Themes. Statt light und dark kann ein Theme-System ocean, forest, contrast-high und print unterstützen – jedes Theme überschreibt dieselben semantischen Tokens mit anderen Werten. Der data-theme-Selector-Ansatz ermöglicht beliebig viele Themes ohne Tailwind-Konfigurationsänderung: [data-theme="ocean"], [data-theme="forest"] und so weiter.
Das useTheme-Composable muss dafür nur die Theme-Liste kennen und die Typ-Definition anpassen. Die CSS-Schicht übernimmt die gesamte visuelle Implementierung. Das ermöglicht, Themes ohne JavaScript-Änderungen hinzuzufügen – ein neues CSS-Token-Set für ein neues Theme registrieren, und es steht sofort im Theme-Switcher zur Verfügung. High-Contrast-Themes für Barrierefreiheit folgen demselben Muster und können sogar das prefers-contrast: high-Media-Query als automatisches Aktivierungskriterium nutzen.
8. Das Flash-of-Wrong-Theme-Problem bei SSR lösen
Das Flash-of-Wrong-Theme (FOWT) ist das bekannteste Problem bei Dark Mode mit SSR. Der Server rendert HTML ohne Kenntnis des Nutzer-Themes. Der Browser zeigt kurz das falsche Theme (meist Light Mode), bevor JavaScript das Theme aus LocalStorage liest und das DOM aktualisiert. Das Ergebnis: ein sichtbares Flimmern beim Seitenaufladen, das als professionell mangelhaft wahrgenommen wird.
Die robusteste Lösung für das FOWT-Problem ist ein inline Script im <head>, das vor dem Rendern ausgeführt wird. Dieses Script liest LocalStorage und setzt das data-theme-Attribut synchron, bevor der Browser das Rendering startet. Da es im <head> steht und kein defer oder async hat, blockiert es kurz das Parsing – aber das ist in diesem Fall gewollt, weil es das Theme-Flimmern verhindert. In Nuxt wird dieses Script über useHead() mit script: [{ children: themeInitScript, tagPosition: 'head' }] eingefügt.
Die Cookie-basierte Alternativlösung sendet das Theme-Präferenz-Cookie bei jedem Request mit. Der Server liest den Cookie und rendert das korrekte data-theme-Attribut direkt im HTML. So sieht der Nutzer nie das falsche Theme – selbst bei deaktiviertem JavaScript. Das ist die technisch sauberste Lösung für das FOWT-Problem, erfordert aber serverseitige Cookie-Verarbeitung, was bei statischen Hosting-Umgebungen nicht möglich ist.
9. Theme-Strategien im Vergleich
Die Wahl der richtigen Dark-Mode- und Theming-Strategie hängt von den Projektanforderungen ab. Für einfache Projekte mit wenigen Komponenten reicht Tailwinds Dark-Mode-Klassen ohne Design Tokens. Für mittlere bis große Projekte sind CSS Custom Properties als Design Tokens der richtige Ansatz. Für SSR-Projekte ist Cookie-Persistenz die einzige Methode, die FOWT vollständig verhindert.
| Strategie | Skalierung | SSR-Flash | Mehrere Themes |
|---|---|---|---|
| Tailwind dark: Klassen | Schlecht (hundert Stellen) | Flash möglich | Nicht unterstützt |
| CSS Custom Properties | Exzellent | Flash mit inline Script | Beliebig viele Themes |
| Cookie + SSR | Exzellent | Kein Flash | Unterstützt |
| Media Query only | Gut | Kein Flash | Nur system-Präferenz |
| Design Tokens + Pinia | Exzellent | Inline Script nötig | Vollständig |
Die empfohlene Kombination für neue Vue-3-Projekte mit Dark Mode: CSS Custom Properties als Design Tokens, Tailwind mit darkMode: ['class', '[data-theme="dark"]'] für Utility-Klassen, ein useTheme-Composable oder Pinia-Store für die Vue-seitige State-Verwaltung, und ein inline Script im <head> für FOWT-Prevention. Bei SSR mit Nuxt: Cookie-Persistenz statt LocalStorage.
Mironsoft
Vue-3-Theming, Design-Token-Systeme und Dark-Mode-Integration
Professionelles Theming für eure Vue-3-App?
Wir implementieren ein vollständiges Design-Token-System mit Dark Mode, System-Präferenz-Erkennung, Persistenz und FOWT-Prevention – für Tailwind-CSS- und CSS-Custom-Properties-basierte Vue-3-Apps.
Token-System
CSS Custom Properties Design-Token-Hierarchie für konsistentes Theming über alle Komponenten
Dark Mode
useTheme-Composable, System-Präferenz, Persistenz und FOWT-Prevention implementieren
Multi-Theme
Erweitertes Theme-System für Branding, Barrierefreiheit und Custom-Themes aufbauen
10. Zusammenfassung
Ein professionelles Vue Dark Mode- und Theming-System basiert auf drei Schichten: CSS Custom Properties als Design Tokens für die visuelle Implementierung, ein useTheme-Composable für die Vue-seitige Reaktivität und State-Verwaltung, und Persistenz plus FOWT-Prevention für eine nahtlose Nutzererfahrung. Design Tokens machen Theme-Wechsel zu einer einzeiligen CSS-Änderung statt einer hundertfachen Komponenten-Modifikation. Das useTheme-Composable kapselt System-Präferenz-Erkennung, Toggle-Logik und DOM-Synchronisation.
Das Flash-of-Wrong-Theme-Problem ist das häufigste Qualitätsproblem bei Dark Mode-Implementierungen und löst sich mit einem inline Script im <head> oder Cookie-Persistenz für SSR. Tailwind CSS und CSS Custom Properties sind keine konkurrierenden Ansätze – sie ergänzen sich: Tokens für systemweite Designentscheidungen, Tailwind-Utilities für komponentenspezifische Anpassungen. Das resultierende System skaliert von zwei Themes auf beliebig viele, ohne die Komponentenebene anzufassen.
Vue Dark Mode und Design Tokens — Das Wichtigste auf einen Blick
Design Token Hierarchie
Primitive Tokens → semantische Tokens → Komponenten-Tokens. Dark Mode überschreibt nur semantische Ebene. Beliebige Themes ohne Komponenten-Änderungen.
useTheme-Composable
System-Präferenz via matchMedia, DOM-Sync via watchEffect, Persistenz via LocalStorage. Modulsope-State für globales Sharing.
FOWT-Prevention
Inline Script im head vor erstem Render. Bei SSR: Cookie-Persistenz für server-seitiges Theme-Rendering ohne Flash.
Tailwind-Integration
darkMode: class mit data-theme-Selektor. CSS Custom Properties als Tailwind-Color-Mapping. Tokens und Utilities ergänzen sich.