メインコンテンツへ飛ぶ

ダークモード

概要

Automatically update the native interfaces

"Native interfaces" include the file picker, window border, dialogs, context menus, and more - anything where the UI comes from your operating system and not from your app. The default behavior is to opt into this automatic theming from the OS.

Automatically update your own interfaces

If your app has its own dark mode, you should toggle it on and off in sync with the system's dark mode setting. これは prefers-color-scheme CSS メディアクエリを使うことでできます。

Manually update your own interfaces

If you want to manually switch between light/dark modes, you can do this by setting the desired mode in the themeSource property of the nativeTheme module. This property's value will be propagated to your Renderer process. Any CSS rules related to prefers-color-scheme will be updated accordingly.

macOS settings

macOS 10.14 Mojave にて、Apple は新しい システム全体のダークモード を全ての macOS コンピュータに導入しました。 If your Electron app has a dark mode, you can make it follow the system-wide dark mode setting using the nativeTheme api.

macOS 10.15 Catalina にて、Apple は新しい "自動" ダークモードオプションを全ての macOS コンピュータに導入しました。 Catalina 上のこのモードで nativeTheme.shouldUseDarkColorsTray API を正しく動作させるには、Electron >=7.0.0 を使用するか、古いバージョンの場合は Info.plist ファイルの NSRequiresAquaSystemAppearancefalse に設定する必要があります。 Electron PackagerElectron Forge の両方に、アプリのビルド時に Info.plist の変更を自動化する darwinDarkModeSupport オプション があります。

Electron > 8.0.0 を使用中でオプトアウトしたい場合は、Info.plist ファイルの NSRequiresAquaSystemAppearance キーを true に設定する必要があります。 Electron 8.0.0 以降では macOS 10.14 SDK を使用するため、このテーマ設定をオプトアウトすることはできません。ご注意ください。

サンプル

ここでは、nativeTheme から派生したテーマカラーになる Electron アプリケーションの例を示します。 加えて、IPC チャンネルを利用したテーマの切り替えとリセットの制御もできます。

const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron/main')
const path = require('node:path')

function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}

ipcMain.handle('dark-mode:toggle', () => {
if (nativeTheme.shouldUseDarkColors) {
nativeTheme.themeSource = 'light'
} else {
nativeTheme.themeSource = 'dark'
}
return nativeTheme.shouldUseDarkColors
})

ipcMain.handle('dark-mode:system', () => {
nativeTheme.themeSource = 'system'
})

app.whenReady().then(() => {
createWindow()

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

これはどのように動作しているのでしょうか?

index.html ファイルから見ていきましょう。

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
<h1>Hello World!</h1>
<p>Current theme source: <strong id="theme-source">System</strong></p>

<button id="toggle-dark-mode">Toggle Dark Mode</button>
<button id="reset-to-system">Reset to System Theme</button>

<script src="renderer.js"></script>
</body>
</html>

そして styles.css ファイルです。

styles.css
@media (prefers-color-scheme: dark) {
body { background: #333; color: white; }
}

@media (prefers-color-scheme: light) {
body { background: #ddd; color: black; }
}

この例では、一対の要素を持つ HTML ページを描画しています。 <strong id="theme-source"> 要素は現在選択されているテーマを示すもので、2 つの <button> 要素は制御用です。 CSS ファイルでは、prefers-color-scheme のメディアクエリを使用して <body> 要素の背景色とテキスト色を設定しています。

preload.js スクリプトで、window オブジェクトに darkMode という新しい API を追加します。 この API は、'dark-mode:toggle''dark-mode:system' の 2 つの IPC チャンネルをレンダラープロセスへ公開します。 また、レンダラープロセスからのメッセージをメインプロセスに渡すため、togglesystem の 2 つのメソッドも代入しています。

preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('darkMode', {
toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
system: () => ipcRenderer.invoke('dark-mode:system')
})

これで、レンダラープロセスはメインプロセスと安全に通信し、nativeTheme オブジェクトに必要な変更操作ができます。

renderer.js ファイルは、<button> の機能を制御する役割を担います。

renderer.js
document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
const isDarkMode = await window.darkMode.toggle()
document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light'
})

document.getElementById('reset-to-system').addEventListener('click', async () => {
await window.darkMode.system()
document.getElementById('theme-source').innerHTML = 'System'
})

addEventListener を使って、renderer.js ファイルで 'click' イベントリスナー を各ボタン要素に追加します。 各イベントリスナーハンドラーには、それぞれの window.darkMode API メソッドの呼び出しをさせます。

最後に、main.js ファイルでメインプロセスを記述し、実際の nativeTheme の API を入れます。

const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')

ipcMain.handle('dark-mode:toggle', () => {
if (nativeTheme.shouldUseDarkColors) {
nativeTheme.themeSource = 'light'
} else {
nativeTheme.themeSource = 'dark'
}
return nativeTheme.shouldUseDarkColors
})

ipcMain.handle('dark-mode:system', () => {
nativeTheme.themeSource = 'system'
})
}

app.whenReady().then(() => {
createWindow()

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

ipcMain.handle メソッドは、HTML ページ上のボタンからのクリックイベントに対して、メインプロセスが応答する手段となります。

'dark-mode:toggle' IPC チャンネルハンドラーのメソッドは、shouldUseDarkColors 真偽値型プロパティを確認して対応する themeSource を設定し、現在の shouldUseDarkColors プロパティを返します。 この IPC チャンネルに対応するレンダラープロセスのイベントリスナーへと戻って見てみると、このハンドラーの戻り値を利用して <strong id='theme-source'> 要素に正しいテキストを代入しています。

'dark-mode:system' IPC チャンネルハンドラーのメソッドは、文字列'system'themeSource に割り当てるだけで何も返しません。 これは対応するレンダラープロセスのイベントリスナーにも言えることで、このメソッドは戻り値が期待できない状態で待機しています。

Electron Fiddle を使ってサンプルを実行してみましょう。"Toggle Dark Mode" ボタンをクリックすると、アプリの背景色が明るくなったり暗くなったりするでしょう。

ダークモード