【JavaScript】【PHP】【WordPress】ダークモードの実装

主に WordPress や PHP のフレームワークで使えるダークモードの切替ボタンを作成しましたので共有します。

これは数あるダークモード実装の一例であり、私がフロントエンドが不得意で改良の余地がある点や、その他の技術的問題点がありますので末尾の注意点もお読みください。

やりたいこと

  • 通常モードとダークモードを切り替えるボタンを作る
  • OS 側の設定や前回訪問時のカラーモードを判定してページ読み込み時にスタイルに反映する

スタイルの切り替えボタンは、アクセシビリティへの対応として古来からある方法です。CSS を複数用意し、JavaScript でスイッチする手法がとられていましたが、今回は比較的新しい CSS の書き方でスタイルを書き、OS でダークモード設定になっていた場合は自動でダークモードになる対応を行います。

仕様

  • OS側のダークモード設定を判定し、初回訪問から色が変わるようにする
  • サイト内にダークモードの切り替えボタンを設置し、手動切替も可能にする
  • 切り替えボタンの判定結果を Cookie に保存し、次回訪問時に同じ設定を反映する

使用するツール・技術

js-cookie の Cookie 操作と、jQuery の属性値の書き換えは、お好みの技術やライブラリに置き換えて構いません。

CSS のカスタムプロパティは IE11 を含む Internet Explorer シリーズでは未実装であるため、CSSが反映されなくなります。カスタムプロパティは作り手の利便性向上のために使っているので、IE に対応する場合は使用しなくても構いません。

お使いのシステムが PHP 以外であれば、後述の PHP コードと同様のロジックをそのシステムに応じて組み込む必要があります。

手動切替ボタンの仕組み

  • input 要素のチェックボックスを利用する
  • チェックボックスにチェックされていればダークモード
  • checked 属性が checked だったらダークモード
  • after/before 疑似要素で通常モードとダークモード用の装飾を行う

実装

HTML

<div class="color-mode">
    <input id="color-mode" name="dark"  type="checkbox">
    <label for="color-mode"></label>
</div>

CSS

通常色/ダークモードの切り替え用

カスタムプロパティ(CSS変数)を使用します。全ての要素に対して通常モードとダークモードの2つの色を指定するのは大変なので、色に関する部分を中心に変数を設定し、モードに応じて色が変わるようにします。

/* 通常モード */
:root {
    --body-bg: #fff;
    --text-color: #000;
}

/* ダークモード */
[data-mode='dark'] {
    --body-bg: #000;
    --text-color: #fff;
}
body {
    background: var(--body-bg);
}

article {
    color: var(--text-color);
}

切り替えボタン用

こういうデザインになる CSS です。私はこれが精一杯でしたので、自分好みのデザインを追及してみてください。

/* カラーモード切り替え */
.color-mode {
    margin: 0 10px 10px 0;
    text-align: right;
    float: right;
}

.color-mode input {
    display: none;
}
.color-mode label {
    font-size: 13px;
    cursor: pointer;
}

/* ダークモードOFF */
.color-mode label:before {
    border: 2px solid #d0d0d0;
    border-right: 0;
    border-radius: 50px 0 0 50px;
    padding: 9px 5px;
    content: '暗';
    background: #fff;
    color: #777;
    font-weight: normal;
    width: 10px;
    text-align: center;
    display: inline-block;
}
/* ライトモードON*/
.color-mode label:after {
    border: 2px solid #d3aa02;
    border-left: 0;
    border-radius: 0 50px 50px 0;
    padding: 9px 5px;
    content: '明';
    background: #ffe168;
    color: #333333;
    width: 10px;
    text-align: center;
    display: inline-block;

}
.color-mode input + label:hover:before {
    background: #ffffff;
    color: #777777;
}
/* ダークモードON */
.color-mode input:checked + label:before {
    border: 2px solid #3372df;
    border-right: 0;
    background: #34b8f8;
    color: #000;
    font-weight: bold;
    display: inline-block;
    text-align: center;
}

/* ライトモード OFF */
.color-mode input:checked + label:after {
    border: 2px solid #d0d0d0;
    border-left: 0;
    background: #fff;
    font-weight: normal;
}
.color-mode input:checked + label:hover:after {
    background: #fff;
}

JavaScript

ライブラリの読み込み

bodyタグ開始より上(headタグ内)で読み込みます。

<script type='text/javascript' src='jquery.js'></script>
<script type='text/javascript' src='js.cookie.min.js'></script>

設定の記述

ボタン用のHTMLの下(body閉じタグの直前)で読み込みます。

<script>
var toggle_color = ''
var os_color = ''
const saved_value = Cookies.get('color_theme_value');

// 
if (!saved_value) {
    // Cookieが存在しなければOSのダークモード判定
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.documentElement.setAttribute('data-mode', 'dark')
        var os_color = 'dark'
        // Cookieが存在しなければトグル用のチェックボックスをcheckedにする
        jQuery('#color-mode').prop('checked', true);
    } else {
        document.documentElement.setAttribute('data-mode', 'light')
        var os_color = 'light'
    }
} else {
    if (saved_value=='dark') {
        document.documentElement.setAttribute('data-mode', 'dark')
        //var os_color = 'dark'
    } else {
        document.documentElement.setAttribute('data-mode', 'light')
        //var os_color = 'light'
    }
}

window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
  if (e.matches) {
    document.documentElement.setAttribute('data-mode', 'dark')
  } else {
    document.documentElement.setAttribute('data-mode', 'light')
    var color = 'light'
  }
});

// トグルボタンでダークモード切り替え
const btn = document.querySelector("#color-mode");
 
btn.addEventListener("change", () => {
  if (btn.checked == true) {
    document.documentElement.setAttribute('data-mode', 'dark')
    toggle_color = 'dark'
  } else {
    document.documentElement.setAttribute('data-mode', 'light')
    toggle_color = 'light'
  }
});

// Cookie保存
jQuery("#color-mode").change(function(e){
    if (!toggle_color) {
        save_value = os_color
    } else {
        save_value = toggle_color
    }
    Cookies.set('color_theme_value', save_value , { expires: 7 });
});
</script>

PHP

Cookie の状態を判定して、ダークモードの場合はページ読み込み時に checked 属性を付与する関数です。HTML の input 要素の中でこの関数を呼び出します。

function dark_mode_checked() {
    if ( $color_theme_value = $_COOKIE['color_theme_value'] ) {
        if ( 'dark' === $color_theme_value ) {
            echo 'checked';
        } else {
            echo '';
        }
    } else {
        echo '';
    }
}
<div class="color-mode">
    <input id="color-mode" name="dark" <?php dark_mode_checked(); ?> type="checkbox">
    <label for="color-mode"></label>
</div>

この仕組みの注意点

ページキャッシュに非対応

JavaScript と CSS だけで完結せず、PHP の挙動を利用しているため、ページをキャッシュする場合は意図しない挙動をとる場合があります。要件によっては致命的です。たとえば以下の例が挙げられます。

  • Proxy キャッシュFastCGI キャッシュなどの Web サーバー組み込みのキャッシュ
  • WordPress のキャッシュプラグインなど Web サーバーより下で動くキャッシュ
  • WordPress の Transient API のような、出力を DB などに一時保存する仕組み
  • CloudFrontCloudflare など Webサーバーの前段に構えるキャッシュ

そもそも PHP 側で判定しているのは、JavaScript の遅延によるスタイルのちらつきを解消するため、サーバー応答の段階でチェック状態を HTML に反映したいからです。これが JavaScript や CSS 側で対応できれば、PHP は不要になります。

IE に非対応

比較的新しい概念を使用しているため、IE に完全対応させるには技術の置き換えが必要になります。

  • IE9 (window.matchMedia に非対応のため、モードの判別が不能)
  • IE11 (CSS 変数に非対応のため、モードの切替が不能)

トレンド観察が必要

ダークモードがアクセシビリティとして普遍的な対応となるのか、トレンドとして今後変わりゆくものなのか分かりません。科学的にダークモードより目に良い配色・レイアウトなどが広まった場合は、過去の流行に留まる可能性もあります。このため、ダークモード機能のメンテナンスは技術面に加え、UI のトレンドにも耳を傾けていく必要があります。個人的には、ダークモードに対応した OS やアプリは即有効にするほど好きな機能です。