Electron で macOS のダークテーマ切替機能を実装する

Electron で、macOS アプリケーションを開発するにあたって、一考するべきものとして「ダークテーマ」の切り替え実装でしょう。Electron には、macOS のシステムとしてライト／ダークのどちらが設定されているか？を知る方法、またユーザーによってリアルタイムに macOS の「外観」が変更されたときのイベント発火も用意されています。

macOS のダークモードのサポート - Electron ドキュメント
<https://www.electronjs.org/docs/tutorial/mojave-dark-mode-guide>

ただ、「どのようなダーク（またはライト）モードにするか？」という部分は、開発者自身で考えてデザイン、変更するコードを書かねばなりません。

具体的な実行イメージとしては、ダークモードへ変更されたとき、HTML で構成された UI は、適用された CSS ファイルを切り替える感じなのだろうかと想像しますが、複数のデザインを切り替えるならともかく、「ライトモード」と「ダークモード」の二種類なら、実際２ファイルの CSS を用意するよりも、後述の「CSS 変数」を使う方が、後のデザイン変更や微調整などのメンテナンスが容易になります。

## サンプルアプリケーション

まずは、この記事を書くにあたって（というよりテストするにあたって）サンプルアプリケーションを作成しました。

hibara/Erectron-Dark-Mode-Test - GitHub
<https://github.com/hibara/Erectron-Dark-Mode-Test>

`node_modules` は、アップロードされておりません。GitHub から `pull` してきたら、カレントディレクトリーで、`npm install` とコマンドを入力し、インストールしてからビルドしてください。

|ライトモード|ダークモード|
|---|---|
|![screen-shot-light.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/f20925ca-46a6-904c-0966-ca7054913e3a.png)|![screen-shot-dark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/b5f85718-a2ce-087f-3fc7-ce1f4779210e.png)|

参考にしたのは、このサイトです。
<https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8>

レイアウトは、これをベースに改良しています。

FLAT SIDE MENU NAVIGATION - CodePen
<https://codepen.io/envoy47/pen/pOzmKa>

## 実行ファイル

わざわざビルドする必要もなく、アプリケーションの動作のみを確認できれば良いという方は、同様に GitHub の以下のページからアプリのみをダウンロードしてお試しください。現在のところは、macOS版のみのビルド配布となっております。

ErectronDarkModeTest-darwin-x64.zip
<https://github.com/hibara/Erectron-Dark-Mode-Test/releases>

## UI テーマは、CSS ファイル切り替えるのではなく「CSS 変数」を使うと便利

「CSS 変数」だけでなく、「カスタムプロパティ」や「カスケード変数」と呼ばれることもあるそうですが、ほとんどのモダンブラウザで使用可能です。
<https://developer.mozilla.org/ja/docs/Web/CSS/Using_CSS_custom_properties>

もちろん、Electron はモダンブラウザである、Chromium が使われているので問題ありません。

たとえば、`--main-color: black;` によって設定し、 var() 関数（たとえば、`color: var(--main-color);`）を使ってアクセスします。

今回のサンプルアプリケーションでは、スタイルシート冒頭にマテリアルデザインを意識した、ベースとなるグラデーションを定義しました。

```css:style.css
:root {
  --base-color-00: #333333;
  --base-color-01: #494949;
  --base-color-02: #606060;
  --base-color-03: #777777;
  --base-color-04: #8d8d8d;
  --base-color-05: #a4a4a4;
  --base-color-06: #bbbbbb;
  --base-color-07: #d1d1d1;
  --base-color-08: #e8e8e8;
  --base-color-09: #ffffff;
```

コードだと、よく分かりませんが、画像で表すと次のような感じです。
![マテリアルカラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/44370f2d-511d-45c7-7121-565e87924236.png)
ライトモードがデフォルトだと思いますので、CSS ファイルの、`:root { }` に含めてしまいます。

```css:style.css
:root {
  /* -- ライトモード・カラー -- */
  --font-color: var(--base-color-00);
  --main-background-color: var(--base-color-09);
  --header-background-color: var(--base-color-08);
  --header-font-color: var(--base-color-00);
  --left-menu-background: var(--base-color-07);
  --left-menu-border: var(--base-color-06);
  --left-menu-sub-background: var(--base-color-08);
  --left-menu-top-hover: var(--base-color-06);
  --left-menu-sub-hover: var(--base-color-09);
  --toggle-switch-theme-on-font: var(--base-color-01);
  --toggle-switch-theme-off-font: var(--base-color-06);
  --toggle-switch-theme-on-color: var(--base-color-09);
  --toggle-switch-theme-off-color: var(--base-color-03);
  --toggle-switch-ball: var(--base-color-08);
  --toggle-switch-ball-disabled: var(--base-color-05);
  --toggle-switch-background-color: var(--base-color-09);
  --toggle-switch-background-color-disabeled: var(--base-color-06);
  --toggle-switch-icon: var(--base-color-00);
  --toggle-switch-box-shadow: var(--base-color-03);
  --scrollbar-track-piece: var(--base-color-07);
  --scrollbar-track-thumb: var(--base-color-05);
  --sheet-background-color: var(--base-color-09);
```

それに対して、「ダークモード」の設定も行います。

```css:style.css
/* -- ダークモード・カラー -- */
[data-theme="dark"] {
  --font-color: var(--base-color-09);
  --main-background-color: var(--base-color-00);
  --header-background-color: var(--base-color-01);
  --header-font-color: var(--base-color-09);
  --left-menu-background: var(--base-color-02);
  --left-menu-border: var(--base-color-00);
  --left-menu-sub-background: var(--base-color-03);
  --left-menu-top-hover: var(--base-color-04);
  --left-menu-sub-hover: var(--base-color-05);
  --toggle-switch-theme-on-font: var(--base-color-01);
  --toggle-switch-theme-off-font: var(--base-color-06);
  --toggle-switch-theme-on-color: var(--base-color-09);
  --toggle-switch-theme-off-color: var(--base-color-03);
  --toggle-switch-ball: var(--base-color-08);
  --toggle-switch-ball-disabled: var(--base-color-05);
  --toggle-switch-background-color: var(--base-color-00);
  --toggle-switch-background-color-disabeled: var(--base-color-06);
  --toggle-switch-icon: var(--base-color-09);
  --toggle-switch-box-shadow: var(--base-color-03);
  --scrollbar-track-piece: var(--base-color-07);
  --scrollbar-track-thumb: var(--base-color-02);
  --sheet-background-color: var(--base-color-01);
}
```

ダークモード・カラーの、CSS ファイル内にある `[data-theme="dark"] { }` は、属性セレクターの値に一致するすべての要素に適用します。

属性セレクター
<https://developer.mozilla.org/ja/docs/Web/CSS/Attribute_selectors>

この例の場合、「要素」の指定がありませんが、アスタリスク（\*）を先頭に付けても同じ結果になります。いわば、[data-theme="dark"] = \*[data-theme="dark"] ということになります。

実際の「ダークモード」の切替は、`index.html` に設置されているトグルスイッチで行います。

```javascript:index.html
const toggleSwitch = $('#checkbox-dark-toggle');
toggleSwitch.change((e) => {
  if (e.target.checked) {
    doc.attr('data-theme', 'dark');   // ダークモードに設定
  }
  else {
    doc.attr('data-theme', 'light');  // ライトモードに設定
  }
})
```

jQuery を使っているのなら上記のように、`attr` で値を指定してください。もし、jQuery を使っていないならば、

```javascript
document.getElementById('checkbox-dark-toggle').setAttribute('data-theme', 'dark');
```

とでも、してください。

## SVG イメージのカラー操作

ダークモード切り替えのとき、単純なアイコン等の画像なら、ファイルの `.svg` 形式を使うと、カラー変更できて便利です。

実際、サンプルアプリケーションで使っているのは、以下の SVG 画像です。
![moon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/4ed77e72-dcc8-4133-8adc-0d23d1534a09.png)
これを HTML 内のソースに組み込むには、`moon.svg` をテキストエディターなどで開きます。

```xml:moon.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
    <g id="moon-icon" transform="matrix(1,0,0,1,16,16)">
        <g>
            <path d="M459.782,347.328C455.494,342.048 448.294,340.096 441.958,342.368C424.198,348.736 404.934,352 384.646,352C287.59,352 208.646,273.024 208.646,176C208.646,117.6 237.478,63.232 285.766,30.528C291.238,26.816 293.862,20.128 292.39,13.696C290.918,7.264 285.638,2.4 279.078,1.44C271.59,0.352 264.134,0 256.646,0C124.294,0 16.646,107.648 16.646,240C16.646,372.352 124.294,480 256.646,480C340.646,480 417.062,437.312 460.998,365.824C464.55,360.032 464.038,352.64 459.782,347.328Z" style="fill-rule:nonzero;"/>
        </g>
    </g>
</svg>
```

ファイル表示としては必要ですが、HTML ソース内に組み込むには不要な記述がたくさんあるので、以下のように削り、修正します。

```xml:moon.svg
<svg display="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol id="moon-icon" viewBox="0 0 512 512">
    <g>
      <path d="M459.782,347.328C455.494,342.048 448.294,340.096 441.958,342.368C424.198,348.736 404.934,352 384.646,352C287.59,352 208.646,273.024 208.646,176C208.646,117.6 237.478,63.232 285.766,30.528C291.238,26.816 293.862,20.128 292.39,13.696C290.918,7.264 285.638,2.4 279.078,1.44C271.59,0.352 264.134,0 256.646,0C124.294,0 16.646,107.648 16.646,240C16.646,372.352 124.294,480 256.646,480C340.646,480 417.062,437.312 460.998,365.824C464.55,360.032 464.038,352.64 459.782,347.328Z" style="fill-rule:nonzero;"/>
    </g>
  </symbol>
</svg>
```

あらかじめ、svg タグの中にある、`display="none"` にして HTML ソース内に記述しておくのがミソです。HTML ファイル中に埋め込まれていますが、非表示の状態になります。

このアイコンは、実際ライトテーマ／ダークテーマの切り替えスイッチのアイコンに使われています。

そして、HTML 冒頭に記述した SVG ファイルデータは、以下のように使います。

```html:index.html
<svg class="dark-mode-svg">
  <use x="0" y="0" xlink:href="#moon-icon"></use>
</svg>
```

スタイルシートでは、以下のように「月」マークの部分の色を `fill` で指定します。

```css:style.css
.dark-mode-svg {
  fill: #333333;  /* 黒色 */
  margin-top: 2px;
  width: 16px;
  height: 16px;
}
```

基本的に、この SVG 画像は、背景が透明ですので、配置された要素の `background` カラーの上に表示されます。

つまり、月のマークの色のみ `fill` の値を変え、そこだけ色を変えています。

## macOS の「設定」から「外観」を変更されたときの処理

macOS の「設定」から、「外観」をリアルタイムで変更できます。対応アプリケーションなら、自動的にライト／ダークが切り替わると思います。

もちろん、Electron にも、その設定変更イベント発火を感知する仕組みが備わっています。

nativeTheme - Electron ドキュメント
<https://www.electronjs.org/docs/api/native-theme#event-updated>

```javascript:main.js
const nativeTheme = require('electron');
nativeTheme.on("updated", () => {
  // macOS の「設定」→「一般」から「外観」を変更されたときのイベント発火
});
```

ただ、この辺りの実装は、主にアプリケーション側にあると思います。「自動」「ライト」「ダーク」のいずれかをユーザーに選択させ、もし「自動」を設定しているのならば、macOS 全体の「外観」カラーに合わせて、アプリケーションの方の色も変更されるべきでしょう。

手動で「ライト」「ダーク」が選択されているならば、このイベント発火は無視しても良いでしょう。

## ダークモード設定の保存・読み込み

ここででは、ダークモード切り替えの説明がメインですので、設定保存は HTML5 の「localStorage」を使います。もう少し本格的に設定の永続化を行いたい場合は適宜、「[electron-store](https://github.com/sindresorhus/electron-store)」を使うなどしてください。

読み込みは、`document` が準備できた段階で良いでしょう。

```javascript:index.html
$(document).ready(function(){
  // ローカルストレージから「ダークモード」の設定を読み込む
  const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
  //・・・
})
```

保存は、「ダークモード」のトグル切替スイッチが変更される度に行います。

```javascript:index.html
// ダークモード切り替えスイッチ
const toggleSwitch = $('#checkbox-dark-toggle');
toggleSwitch.change((e) => {
  if (e.target.checked) {
    doc.attr('data-theme', 'dark');
    localStorage.setItem('theme', 'darks');
  }
  else {
    doc.attr('data-theme', 'light');
    localStorage.setItem('theme', 'light');
  }
})
```

## メニューバー（Tray）のダークモード切り替え対応

忘れがちですが、メニューバーに独自アイコンを表示している場合は、ダークモード切り替えに対応しておく必要があります（カラーアイコンで両対応できてるのなら問題無し）。

もしマルチプラットフォームで開発し、Windows 10 の場合には、macOS の「ダークモード」に合わせた `Tray` アイコンを使った方が無難です。

ちなみに、Electron の `Tray` を使ったアプリケーションの実装については、別の記事で書きました。よろしければご参考にしてください。

参考：**Electron で Tray を使う（macOS, Windows）**
<https://qiita.com/hibara/items/4a3c26817e5449ebf722>