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

JavaScript
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

JavaScript
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":

JavaScript
// 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:

CSS renderer.css
:root {
    --bg: #ffffff;
    --fg: #1a1a1a;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #1a1a1a;
        --fg: #f0f0f0;
    }
}

body {
    background: var(--bg);
    color: var(--fg);
}
JavaScript renderer.js
// 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

JavaScript main.js
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
            });
        });
    });
});
JavaScript preload.js
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:

CSS
@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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Main-Prozess

Zur Übersicht