モダンなダークモードトグルをJavaScriptとCSS変数で実装する方法

目次

はじめに

最近のWebサイトでは、ユーザーの好みに応じてダークモードとライトモードを切り替える機能が当たり前になってきています。しかし、「システム設定と連携させるべきか」「ユーザーの設定を保存するべきか」など、実装時には様々な検討事項があります。

この記事では、CSS変数とJavaScriptを使用して、カスタマイズ可能なダークモードトグルを実装する方法を解説します。システムのカラーテーマとの連携や、LocalStorageを使用した設定の保存まで、実践的な実装方法をお伝えします。

詳しく解説していますので、コードだけほしい方は最後の「完成コード」までスクロールしてください。

見本

今回は以下のようなダークモードトグルを実装していきます。

See the Pen Untitled by ryoma (@hwjgdjpk-the-decoder) on CodePen.

実装におけるポイントとしては以下の通りです。

  • CSS変数による柔軟なカラーカスタマイズ
  • システムのダークモード設定との連携
  • LocalStorageによるユーザー設定の保存

これらの機能を組み合わせることで、モダンなダークモードの実装が可能となります。

ダークモードトグルの見た目の制作

HTML

<body>
  <div class="theme-switch-wrapper">
    <span class="theme-switch-label">ダークモード</span>
    <label class="theme-switch">
      <input type="checkbox" id="checkbox" />
      <div class="slider">
        <div class="slider-icon">
          <svg
            class="sun-icon"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
          >
            <path
              d="M12 3a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0V4a1 1 0 0 1 1-1zm7.07 3.93a1 1 0 0 1 0 1.414l-.707.707a1 1 0 1 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 0zM12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8zm-8.07-1.07a1 1 0 0 1 1.414 0l.707.707A1 1 0 1 1 4.636 9.05l-.707-.707a1 1 0 0 1 0-1.414zM4 12a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1zm.636 5.95a1 1 0 0 1 0-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414 0zM12 19a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1zm7.07-1.07a1 1 0 0 1-1.414 0l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707a1 1 0 0 1 0 1.414zM20 12a1 1 0 0 1-1 1h-1a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z"
            />
          </svg>
          <svg
            class="moon-icon"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
          >
            <path
              d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313-12.454z"
            />
          </svg>
        </div>
      </div>
    </label>
  </div>

  <!-- デモ用のコンテンツ -->
  <div class="content">
    <h1>ダークモードのデモ</h1>
    <p>このテキストの色もテーマに応じて変化します。</p>
  </div>
</body>

トグルスイッチのHTMLについて、重要な要素とその役割を詳しく解説します。

基本構造

<div class="theme-switch-wrapper">
  <span class="theme-switch-label">ダークモード</span>
  <label class="theme-switch">

この部分はトグルスイッチの外側の構造です。wrapperはフレックスボックスとして実装され、ラベルとスイッチを横並びにします。アクセシビリティのため、明示的なラベルテキストを含めています。

トグルの核となる部分

<input type="checkbox" id="checkbox">
<div class="slider">

このチェックボックスが実際のトグル機能を担います。視覚的には非表示(opacity: 0)にしていますが、スクリーンリーダーでは認識可能です。sliderdivは、チェックボックスの上に表示される装飾的なスイッチの外観を提供します。

アイコンの実装

<div class="slider-icon">
  <svg class="sun-icon" ...>
    <path d="M12 3a1 1 0 0 1 1 1v1..." />
  </svg>
  <svg class="moon-icon" ...>
    <path d="M12 3c.132 0 .263 0..." />
  </svg>
</div>

SVGアイコンの詳細は以下の通りです。

  • sun-icon: 16本の光線を持つ太陽を表現しています。path内の複雑なパスデータは、中心から放射状に広がる光線と中心の円を描画します。
  • moon-icon: シンプルな三日月の形状を表現。パスデータは滑らかな曲線で月の輪郭を描画します。

デモコンテンツ

<div class="content">
  <h1>ダークモードのデモ</h1>
  <p>このテキストの色もテーマに応じて変化します。</p>
</div>

このセクションは、カラーテーマの変更効果を視覚的に確認するためのデモ領域です。heading要素とparagraph要素は、CSS変数を通じてテーマ切り替えに連動して色が変化します。

実際に使用するときはこの部分にコンテンツを追加してください。

アクセシビリティへの配慮

  • チェックボックスにid属性を設定し、label要素と紐付けることで、クリック/タップ可能な領域を広げています
  • スクリーンリーダー用のテキストラベルを明示的に提供
  • SVGアイコンは装飾的な要素として扱い、アクセシビリティツリーからは除外

このような構造により、視覚的な美しさとアクセシビリティの両立を実現しています。

CSS

CSSの実装では、カラーテーマの定義をCSS変数で行うことで、柔軟なカスタマイズを可能にしています。

:root {
    --primary-color: #302AE6;
    --secondary-color: #536390;
    --font-color: #424242;
    --bg-color: #fff;
    --heading-color: #292922;
    --switch-bg: #E5E5E5;
    --switch-slider: #fff;
    --transition-time: 0.3s;
}

[data-theme="dark"] {
    --primary-color: #9A97F3;
    --secondary-color: #818CAB;
    --font-color: #e1e1ff;
    --bg-color: #161625;
    --heading-color: #818CAB;
    --switch-bg: #34323D;
    --switch-slider: #202027;
}

body {
    background-color: var(--bg-color);
    color: var(--font-color);
    transition: all var(--transition-time) ease;
}

.theme-switch-wrapper {
    display: flex;
    align-items: center;
    padding: 10px;
}

.theme-switch-label {
    margin-right: 10px;
    font-size: 1rem;
    color: var(--font-color);
}

.theme-switch {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
}

.theme-switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: var(--switch-bg);
    transition: var(--transition-time);
    border-radius: 34px;
}

.slider-icon {
    position: absolute;
    top: 5px;
    left: 5px;
    width: 24px;
    height: 24px;
    transition: var(--transition-time);
}

.sun-icon, .moon-icon {
    position: absolute;
    width: 100%;
    height: 100%;
    fill: var(--font-color);
}

.sun-icon {
    opacity: 1;
}

.moon-icon {
    opacity: 0;
}

input:checked + .slider .slider-icon {
    transform: translateX(26px);
}

input:checked + .slider .sun-icon {
    opacity: 0;
}

input:checked + .slider .moon-icon {
    opacity: 1;
}

/* デモ用のコンテンツスタイル */
.content {
    max-width: 800px;
    margin: 50px auto;
    padding: 20px;
}

h1 {
    color: var(--heading-color);
    transition: color var(--transition-time) ease;
}

p {
    line-height: 1.6;
    margin-top: 1em;
}

トグルスイッチのスタイリングでは、モダンなUIの定番となっているスライド式のデザインを採用しています。

ダークモードトグルの動きの制作

見た目が完成したので、次は実際の動作を実装していきます。システムのダークモード設定を検知し、ユーザーの選択を保存する機能を追加していきましょう。

JavaScriptの完成コード

class ThemeManager {
  constructor() {
    this.checkbox = document.querySelector('#checkbox');
    this.initializeTheme();
    this.setupEventListeners();
  }

  initializeTheme() {
    // システムのカラーテーマを検出
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');

    // LocalStorageから保存された設定を取得
    const savedTheme = localStorage.getItem('theme');

    if (savedTheme) {
      // 保存された設定があればそれを適用
      document.documentElement.setAttribute('data-theme', savedTheme);
      this.checkbox.checked = savedTheme === 'dark';
    } else if (prefersDark.matches) {
      // システム設定がダークモードならそれを適用
      document.documentElement.setAttribute('data-theme', 'dark');
      this.checkbox.checked = true;
    }
  }

  setupEventListeners() {
    // トグル切り替え時の処理
    this.checkbox.addEventListener('change', () => {
      const theme = this.checkbox.checked ? 'dark' : 'light';
      document.documentElement.setAttribute('data-theme', theme);
      localStorage.setItem('theme', theme);
    });

    // システムのカラーテーマ変更検知
    window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (e) => {
        if (!localStorage.getItem('theme')) {
          const theme = e.matches ? 'dark' : 'light';
          document.documentElement.setAttribute('data-theme', theme);
          this.checkbox.checked = e.matches;
        }
    });
  }
}

// 初期化
new ThemeManager();

主要な処理を解説していきます。

初期化プロセスでは、まずLocalStorageに保存されたユーザーの設定を確認します。保存された設定がない場合は、システムのカラーテーマを参照します。これにより、初回訪問時でもユーザーの好みに合わせた表示が可能となります。

イベントリスナーの設定では、トグルスイッチの切り替えとシステムのカラーテーマ変更の両方に対応しています。トグル切り替え時には、選択されたテーマをLocalStorageに保存することで、次回訪問時も同じ設定を維持できます。

クラスベースの実装により、コードの管理が容易になっています。
また、将来的な機能追加にも対応しやすい設計となっています。

完成コード

以下が今回実装したダークモードトグルの完全なコードです。

HTML

<body>
  <div class="theme-switch-wrapper">
    <span class="theme-switch-label">ダークモード</span>
    <label class="theme-switch">
      <input type="checkbox" id="checkbox" />
      <div class="slider">
        <div class="slider-icon">
          <svg
            class="sun-icon"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
          >
            <path
              d="M12 3a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0V4a1 1 0 0 1 1-1zm7.07 3.93a1 1 0 0 1 0 1.414l-.707.707a1 1 0 1 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 0zM12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8zm-8.07-1.07a1 1 0 0 1 1.414 0l.707.707A1 1 0 1 1 4.636 9.05l-.707-.707a1 1 0 0 1 0-1.414zM4 12a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1zm.636 5.95a1 1 0 0 1 0-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414 0zM12 19a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1zm7.07-1.07a1 1 0 0 1-1.414 0l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707a1 1 0 0 1 0 1.414zM20 12a1 1 0 0 1-1 1h-1a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z"
            />
          </svg>
          <svg
            class="moon-icon"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
          >
            <path
              d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313-12.454z"
            />
          </svg>
        </div>
      </div>
    </label>
  </div>

  <!-- デモ用のコンテンツ -->
  <div class="content">
    <h1>ダークモードのデモ</h1>
    <p>このテキストの色もテーマに応じて変化します。</p>
  </div>
</body>

CSS

:root {
    --primary-color: #302AE6;
    --secondary-color: #536390;
    --font-color: #424242;
    --bg-color: #fff;
    --heading-color: #292922;
    --switch-bg: #E5E5E5;
    --switch-slider: #fff;
    --transition-time: 0.3s;
}

[data-theme="dark"] {
    --primary-color: #9A97F3;
    --secondary-color: #818CAB;
    --font-color: #e1e1ff;
    --bg-color: #161625;
    --heading-color: #818CAB;
    --switch-bg: #34323D;
    --switch-slider: #202027;
}

body {
    background-color: var(--bg-color);
    color: var(--font-color);
    transition: all var(--transition-time) ease;
}

.theme-switch-wrapper {
    display: flex;
    align-items: center;
    padding: 10px;
}

.theme-switch-label {
    margin-right: 10px;
    font-size: 1rem;
    color: var(--font-color);
}

.theme-switch {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
}

.theme-switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: var(--switch-bg);
    transition: var(--transition-time);
    border-radius: 34px;
}

.slider-icon {
    position: absolute;
    top: 5px;
    left: 5px;
    width: 24px;
    height: 24px;
    transition: var(--transition-time);
}

.sun-icon, .moon-icon {
    position: absolute;
    width: 100%;
    height: 100%;
    fill: var(--font-color);
}

.sun-icon {
    opacity: 1;
}

.moon-icon {
    opacity: 0;
}

input:checked + .slider .slider-icon {
    transform: translateX(26px);
}

input:checked + .slider .sun-icon {
    opacity: 0;
}

input:checked + .slider .moon-icon {
    opacity: 1;
}

/* デモ用のコンテンツスタイル */
.content {
    max-width: 800px;
    margin: 50px auto;
    padding: 20px;
}

h1 {
    color: var(--heading-color);
    transition: color var(--transition-time) ease;
}

p {
    line-height: 1.6;
    margin-top: 1em;
}

JavaScript

class ThemeManager {
  constructor() {
    this.checkbox = document.querySelector('#checkbox');
    this.initializeTheme();
    this.setupEventListeners();
  }

  initializeTheme() {
    // システムのカラーテーマを検出
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');

    // LocalStorageから保存された設定を取得
    const savedTheme = localStorage.getItem('theme');

    if (savedTheme) {
      // 保存された設定があればそれを適用
      document.documentElement.setAttribute('data-theme', savedTheme);
      this.checkbox.checked = savedTheme === 'dark';
    } else if (prefersDark.matches) {
      // システム設定がダークモードならそれを適用
      document.documentElement.setAttribute('data-theme', 'dark');
      this.checkbox.checked = true;
    }
  }

  setupEventListeners() {
    // トグル切り替え時の処理
    this.checkbox.addEventListener('change', () => {
      const theme = this.checkbox.checked ? 'dark' : 'light';
      document.documentElement.setAttribute('data-theme', theme);
      localStorage.setItem('theme', theme);
    });

    // システムのカラーテーマ変更検知
    window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (e) => {
        if (!localStorage.getItem('theme')) {
          const theme = e.matches ? 'dark' : 'light';
          document.documentElement.setAttribute('data-theme', theme);
          this.checkbox.checked = e.matches;
        }
    });
  }
}

// 初期化
new ThemeManager();

まとめ

今回は、モダンなダークモードトグルの実装方法を解説しました。CSS変数を活用することで、テーマの管理が容易になり、将来的な拡張性も確保できています。また、システムの設定との連携やユーザー設定の保存により、より良いユーザー体験を提供することができます。

このコードは、ブログやポートフォリオサイト、Web アプリケーションなど、様々なプロジェクトで活用できます。必要に応じて色やアニメーションをカスタマイズし、プロジェクトに合わせた実装にしてください。

関連記事

あわせて読みたい
5分でわかる!スニペット登録の方法 コーディングの速度を上げたい コードの再利用やタイピングの手間を減らしたい と思っている人は多いのではないでしょうか? そんな人にとって、スニペットは強い味方に...
あわせて読みたい
3年間のWeb制作で経験したHTMLのつまづきポイント6選!現役コーダーが教える解決方法 3年間のWeb制作で経験したHTMLのつまづきポイント6選 Web制作でHTMLは基本中の基本ですが、意外とつまづきポイントが多いものです。 実際に苦戦している人の声をSNS等で...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次