😎

jQueryとcssで作るダークモードテーマ対応

2022/06/18に公開

今回ご紹介する内容

今回はWebサイトなどでダークモードに対応する方法と動的に配色を切り替えに対応する方法をご紹介いたします。

やりたいこと

  • OSのダークモードにあわせて配色を暗くしたい
  • OSの設定に関係なくライトモード・ダークモードを切り替えたい
  • ユーザが配色の設定をできるようにしたい

https://zenn.dev/harurow/articles/812dabf395797f

サンプル・ソース

全体のソースはGitHubに公開しています。

https://github.com/Harurow/zenn-sample-theme

動作イメージは以下の通りです。

戦略

  • テーマ自体はライトモード、ダークモードをcssで用意
  • 切り替えはhtmlタグの属性にthemeをjQueryで付け替えして行う
  • 環境の変更をイベントハンドラで監視、変更があるたびにthemeを切り替える
  • ユーザによる色の変更はbodyタグのstyleで上書再定義

全体構成をつくる

スタイルシートとして3つ用意します。
style.css 全体の構造用のスタイルシート
theme.css テーマの色を定義するスタイルシート
ripple.css クリック時のアニメーション用のスタイルシート

index.html
... 略 ...
  <link rel="stylesheet" href="style.css" />
  <link rel="stylesheet" href="theme.css" />
  <link rel="stylesheet" href="ripple.css" />
... 略 ...

メイン領域にはクリックできる擬似ボタンを3つ用意。これでテーマを切り替える。
またプライマリ処理を変更できるようにカラーピッカーも追加。

index.html
  <header>
    <span class="title">
      CSSテーマサンプル
    </span>
  </header>
  <main>
    <div class="theme-selector">
      <div class="theme-auto ripple">
        <span>自動/環境に合わせる</span>
      </div>
      <div class="theme-light ripple">
        <span>ライト</span>
      </div>
      <div class="theme-dark ripple">
        <span>ダーク</span>
      </div>
      <div class="theme-color-pallet">
        <label for="primary-color">プライマリ色</label>
        <input id="primary-color" type="color" title=""/>
      </div>
    </div>
  </main>

CSS変数で色を定義

今回テーマによって色が変わるところをcss変数を利用して設定します

変数 説明
--header-background ヘッダーの背景色
--header-color ヘッダーの文字色
--main-background メイン領域の背景色
--main-color メイン領域の文字色
--p-color プライマリーカラー
--p-highlight プライマリーカラーのハイライト
--p-text プライマリーの文字色

大分割愛していますが大まかには以下のようにCSS変数で色を指定しています

style.css
header {
  background-color: var(--header-background);
  color: var(--header-color);
}

main {
  background-color: var(--main-background);
  color: var(--main-color);
}

div.ripple {
  background: var(--p-color);
  color: var(--p-text);
}

div.ripple:hover,
div.ripple:active {
  background: var(--p-highlight);
}

CSS変数の色をテーマ別に設定

テーマカラーのCSS変数は :root 擬似クラスを利用して設定します

theme.css
...略...

/* デフォルトのテーマ。ライトモード時の色 */
:root,
:root[theme="light"] {
  --header-background: #4682b4;
  --header-color: #f5f5f5;
  --main-background: #f5f5f5;
  --main-color: #222222;

  --p-color: #4682b4;
  --p-highlight: #86afd0;
  --p-text: #f5f5f5;
}

/* ダークモード時の色 */
:root[theme="dark"] {
  --header-background: #444444;
  --header-color: #dddddd;
  --main-background: #333333;
  --main-color: #dddddd;

  --p-color: #274863;
  --p-highlight: #437dad;
  --p-text: #f5f5f5;
}

JavaScriptでテーマを定義する

変数 themMode を用意その値で現在のテーマを表す。モードはAuto, Dark, Lightを用意。

index.js
const ThemeMode = {
  Auto: null,           // 自動
  Dark: 'dark',         // ダークモード
  Light: 'light'        // ライトモード
}

let themMode = ThemeMode.Auto   // 初期設定は自動

ブラウザがダークモードかを確認

window.matchMediaを利用してダークモードかを確認する
また変更があった時のイベントハンドラも登録

main.js
// メディアクエリーリスト。ダークモードが設定されているかを確認。
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')

// 環境の変更イベントを監視
mediaQueryList.addEventListener('change', updateTheme)

// テーマモードによってテーマを切り替える
function updateTheme () {
  const theme = themMode ?? getBrowserTheme()
  const htmlElement = document.body.parentElement
  htmlElement.setAttribute('theme', theme)

  ......
}

// メディアクエリーリストの状態に沿ったテーマを取得する
function getBrowserTheme () {
  return mediaQueryList.matches ? 'dark' : 'light'
}

updateTheme() // 初期設定

ボタンのイベントハンドラを登録

擬似ボタンをクリックされたときのハンドラを登録
ボタンに合わせてテーマ設定を行う

main.js
$('.theme-auto').on('click', () => {
  themMode = ThemeMode.Auto   // テーマ設定を自動にする
  updateTheme()               // テーマを更新
})

$('.theme-light').on('click', () => {
  themMode = ThemeMode.Light  // テーマ設定をライトモードで固定
  updateTheme()               // テーマを更新
})

$('.theme-dark').on('click', () => {
  themMode = ThemeMode.Dark   // テーマ設定をダークモードで固定
  updateTheme()               // テーマを更新
})

カラーピッカーのイベントハンドラを登録

計算済みのスタイルを取得し現在のプライマリ色を取得
カラーピッカーの色を取得
色が異なればbodyタグのstyle属性でCSS変数を追加し色を設定する

main.js
$('#primary-color').on('change', () => {
  // 計算済みのスタイルを取得
  const body = document.body
  const computedStyle = getComputedStyle(body)

  // プライマリ色を取得
  const themeRgb = computedStyle.getPropertyValue('--p-color').trim()
  
  // カラーピッカーの色を取得
  const pickerRgb = $('#primary-color').val()

  if (themeRgb !== pickerRgb) {
    // 色が異なる場合は r, g, b を取得
    const r = Number.parseInt(pickerRgb.substring(1, 3), 16)
    const g = Number.parseInt(pickerRgb.substring(3, 5), 16)
    const b = Number.parseInt(pickerRgb.substring(5, 7), 16)

    // hsl方式へ変換
    const { h, s, l } = rgba2hsla({r, g, b})

    // プライマリ色を設定
    body.style.setProperty('--p-color', pickerRgb)

    // プライマリ色のハイライトはhslのlを20%増しで設定
    body.style.setProperty('--p-highlight', `hsl(${h}, ${s}%, ${Math.min(l + 20, 100)}%)`)
  } else {
    // ユーザが設定した色設定を破棄
    clearCustomColorPallet()
  }
})

最後に

以上、ダークモード対応のご紹介でした。カラーピッカーを利用すればユーザが好きに色を変更できるようになります。
RSSリーダのレビューでUIが弱いとのご指摘受けてるのでこれを気に対応したいと考えています。

Discussion