macOS, Windows und moderne Linux-Desktops haben System-weite Hell/Dunkel-Modi. nativeTheme ist Electrons API zum Erkennen und Überschreiben — und der Brückenkopf zur Web-API prefers-color-scheme, die im Renderer ohnehin funktioniert.
System-Theme abfragen
import { nativeTheme } from 'electron';
// Aktuell aktiv
console.log(nativeTheme.shouldUseDarkColors); // true / false
// Was hat der User explizit gewählt?
console.log(nativeTheme.themeSource);
// 'system' (default), 'light', 'dark'shouldUseDarkColors ist die einzig richtige Antwort auf „dunkel oder nicht?" — kombiniert System-Default mit App-Override (themeSource).
Theme-Wechsel beobachten
nativeTheme.on('updated', () => {
const isDark = nativeTheme.shouldUseDarkColors;
console.log('Theme geändert:', isDark ? 'dark' : 'light');
// Renderer informieren
mainWindow.webContents.send('theme:changed', isDark);
});updated feuert sowohl bei System-Wechsel (User klickt im OS Dark/Light) als auch bei App-Override.
App-Override
User-Settings für „Light / Dark / System":
// User wählt im App-Settings
function setTheme(choice) {
// 'system', 'light', 'dark'
nativeTheme.themeSource = choice;
store.set('theme', choice);
}
// Beim App-Start letzte Wahl wieder herstellen
nativeTheme.themeSource = store.get('theme', 'system');Setzen von themeSource triggert das updated-Event automatisch. Praktisch: ein einziger Code-Pfad fürs Theme-Update.
Brücke zum Renderer — prefers-color-scheme
Im Renderer funktioniert die Web-API prefers-color-scheme automatisch und respektiert Electrons themeSource:
:root {
--bg: #ffffff;
--fg: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--fg: #f0f0f0;
}
}
body {
background: var(--bg);
color: var(--fg);
}// JS-Detection im Renderer
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const isDark = mq.matches;
mq.addEventListener('change', (e) => {
console.log('Theme:', e.matches ? 'dark' : 'light');
});Wenn du im Main nativeTheme.themeSource = 'dark' setzt, sieht der Renderer das automatisch via Media Query — keine extra IPC-Kommunikation nötig für reine CSS-Logik.
Vollständiges Pattern
import { app, nativeTheme, ipcMain } from 'electron';
import Store from 'electron-store';
const store = new Store();
app.whenReady().then(() => {
nativeTheme.themeSource = store.get('theme', 'system');
ipcMain.handle('theme:get', () => ({
source: nativeTheme.themeSource,
isDark: nativeTheme.shouldUseDarkColors
}));
ipcMain.handle('theme:set', (_event, source) => {
nativeTheme.themeSource = source;
store.set('theme', source);
});
nativeTheme.on('updated', () => {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('theme:changed', {
source: nativeTheme.themeSource,
isDark: nativeTheme.shouldUseDarkColors
});
});
});
});contextBridge.exposeInMainWorld('theme', {
get: () => ipcRenderer.invoke('theme:get'),
set: (source) => ipcRenderer.invoke('theme:set', source),
onChanged: (cb) => {
const listener = (_event, data) => cb(data);
ipcRenderer.on('theme:changed', listener);
return () => ipcRenderer.removeListener('theme:changed', listener);
}
});High-Contrast und Reduced-Motion
nativeTheme deckt nur Hell/Dunkel ab. Andere System-Präferenzen via Web-API:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-contrast: more) {
:root { --border: 2px; }
}In Production-Apps: alle drei Präferenzen respektieren — Dark/Light, Reduced-Motion, High-Contrast.
Interessantes
shouldUseDarkColors ist die richtige Frage.
Nicht themeSource direkt prüfen — das gibt nur die User-Wahl wieder, ohne System-Auflösung. shouldUseDarkColors kombiniert beides: bei themeSource: 'system' schaut es ins OS, bei 'dark'/'light' ist klar.
CSS macht das Meiste — IPC nur für JS-Logik.
Wenn deine Theme-Logik rein CSS ist (@media (prefers-color-scheme)), brauchst du im Renderer keinen IPC-Listener. Setzt der Main themeSource, sieht der Renderer das via Media Query automatisch. IPC nur für JS-State (z. B. SVG-Icons austauschen).
themeSource wirkt auch auf Title-Bar (Windows 11).
Wenn du themeSource = 'dark' setzt, wechselt auf Windows 11 auch die native Title-Bar in Dark. Praktisch für konsistente Optik. Auf macOS und Linux mit Custom-Titlebar: weniger Effekt, weil sowieso eigenes UI.
System-Theme-Wechsel wird oft ignoriert.
User wechselt im OS Dark Mode, deine App reagiert nicht — typisch fehlender nativeTheme.on('updated')-Listener. Pflicht-Pattern für jede Theme-aware App.
Erste Theme-Wahl beim App-Start aus Settings laden.
Wer themeSource nicht setzt, fällt auf 'system' zurück. Wer User-Wahl persistieren will: aus electron-store lesen und beim Start setzen. Sonst springt die App bei jedem Start auf System-Default zurück.
Window-Background sollte zum Theme passen.
BrowserWindow.backgroundColor setzen — beim Start zeigt der Renderer kurz nichts, der Fenster-Background ist sichtbar. Wenn der weiß ist und das Theme dunkel: Flackern. Pragmatisch: backgroundColor je nach shouldUseDarkColors setzen.