Help us understand the problem. What is going on with this article?

CSS/JavaScriptでダークテーマに対応する方法 (2019/9更新)

More than 1 year has passed since last update.

(2019/9/27追記)

iOS13のアップデートにより、ついにiOS Safariにおいてテーマの取得が可能になりました。


macOS Mojaveで画面を目に優しい黒基調にしてくれる「ダークテーマ」が導入されました。
それ以降、設定したテーマに色調を追従させるようなアプリが続々と出てますね。
こうなるとWebアプリやWebサイトもテーマに合わせたくなります。
今回、開発しているWebアプリ(テーマ切り替え機能自体は導入済み)でmacOSテーマに追従しようとして方法を調べたのでまとめます。

CSS Media Queryで取得する方法

Media Queryで利用できるメディア特性として、prefers-color-schemeというものがあります。
これはユーザーが明色か暗色のどちらを求めているかを教えてくれます。
つい先日リリースされたSafari 12.1からデフォルトで有効になっており、Firefoxは67から対応となっています。
Chromeは実装作業中のようです。

実際にCSSで表示を切り替えたい場合は以下のようにすれば良いです。

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

@media (prefers-color-scheme: dark) {
  body {
    background-color: black;
    color: white;
  }
}

実際はこんな感じでCSS変数にまとめると各所の色を一括して変更できるのでおすすめです。

@media (prefers-color-scheme: light) {
  html {
    --primary-color: black;
    --background-color: white;
  }
}

@media (prefers-color-scheme: dark) {
  html {
    --primary-color: white;
    --background-color: black;
  }
}

html {
  color: var(--primary-color);
  background-color: var(--background-color);
}

JavaScriptで変更を検知する方法

初めてテーマを導入する場合は上記のCSSによる切り替えでいいと思います。
ただJavaScriptによるテーマの切り替えと同時に実装したい場合は、現在の状態をJavaScript側で取得する必要があります。
Media Queryでスタイルを切り替えてwindow.getComputedStyleを使って状態を取得する、という方法でも良いです。
が、OSテーマの切り替え時に即座に反応するためにはポーリングしなくてはならなくなり非効率です。

そこでテーマの切り替えをイベントで取得するためにwindow.matchMediaを使います。
window.matchMediaに通常のMedia Queryの文字列をそのまま渡すとMediaQueryListオブジェクトが手に入ります。

const mql = window.matchMedia('(prefers-color-scheme: dark)')

MediaQueryListオブジェクトのmatchesプロパティがMedia Queryがマッチしたかどうかを真偽値で持っているので、

if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  /* ダークテーマの時 */
} else {
  /* ライトテーマの時 */
}

とりあえずこれで状態の取得が可能です。

イベントを受け取るためには次のようにします。

// ダークテーマの時にマッチするMediaQueryListオブジェクト
const isDark = window.matchMedia('(prefers-color-scheme: dark)')

// コールバック関数はMediaQueryListオブジェクトを受け取る
function toggleTheme (mql) {
  if (mql.matches) {
    /* ダークテーマの時 */
  } else {
    /* ライトテーマの時 */
  }
}

// イベントリスナーを追加
isDark.addListener(toggleTheme)

テーマを切り替えるたびにtoggleTheme関数が呼ばれ、マッチ状態に応じてテーマの切り替え処理を実行することができます。

今回私が書いた環境のVue.js+Vuexだとこんな感じです。

export default {
  name: 'app',
  methods: {
    toggleTheme(mql) {
      if (mql.matches) {
        this.$store.commit('updateTheme', 'dark')
      } else {
        this.$store.commit('updateTheme', 'light')
      }
    }
  },
  mounted() {
    this.$nextTick(() => {
      const isDark = window.matchMedia('(prefers-color-scheme: dark)')
      isDark.addListener(this.toggleTheme)
    })
  }
}

これでどんな感じのテーマ機能が実装できるか置いておきます。
左下の月のアイコンがテーマのトグルボタンですが、JavaScript側にも状態が反映されていることがわかると思います。
CleanShot 2019-04-11 at 20.36.36.gif

trap
東京工業大学で活動するデジタル創作・プログラミング系サークルです。
https://trap.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away