(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側にも状態が反映されていることがわかると思います。

