🐼

Tailwind CSS でダークモードを実装する(React での切り替えボタンの実装コード例あり)

2021/08/20に公開

Tailwind CSS でダークモードを実装する

Tailwind CSS でダークモードを実装する方法を紹介します。Tailwind CSS は v2 からダークモード用の CSS を簡単に適用できるようになりました。

この記事では、以前私が個人開発で作成した note PDFy というサイトを例に、ダークモードの作成方法と Tailwind CSS でのダークモードの実装方法を解説します。

note PDFy というサイトのトップページ

このサイトは note の URL を入力するだけで記事を PDF 化できるツールです。これを使うと note の記事をオフラインで読むことができます。

なお、本記事は私の所属する BASE 社の Frontend Weekly LT で発表した内容を記事化したものです。

CSS でダークモードを実装する方法

まずはダークモードに対応する CSS を設定する方法を解説します。本章の内容は、ダークモード入門 という記事を参考にしています。

ダークモードに対応する方法は 「メディアクエリ」「クラス指定」 以下の2通りです。

それぞれの方法を紹介します。なお、以下では背景色が白基調の設定をダークモードと対比して「ライトモード」と呼ぶことにします。

メディアクエリ

メディアクエリで対応する場合、prefers-color-schemeを使います。

:root {
  --cText: #656765;
}

/* ダークモード時のスタイル */
@media (prefers-color-scheme: dark) {
  :root {
    --cText: #fcfaf2;
  }
}

.text {
  color: var(--cText);
}

これでテキストカラーはライトモードの時に#656765、ダークモードの時に#fcfaf2になります。

メディアクエリで実装する場合は、OS のダークモードの設定を参照するため、ライト/ダークモードを切り替える際には OS の設定を変更する必要があります。

なお、:roothtml タグを指定する擬似クラスです。

MDN曰く「:root はグローバルの CSS 変数を宣言するのに便利」とのことです。

クラスの指定

クラスを指定する場合は CSS の書き方が少し変わります。

:root {
  --cText: #656765;
}

// dark というクラスがある時の色を指定
:root.dark {
 --cText: #64363c;
}

.text {
  color: var(--cText);
}

これで、html タグにdarkというクラス名が付与されていたら、ダークモード用の色が割り当てられるます。

// ライトモード
<html>
  ...
</html>
// ダークモード
<html class="dark">
  ...
</html>

html に dark クラス名が付与されているところ

メディアクエリの場合は OS の設定を変えない限りはライト/ダークモードの切り替えはできませんでした。

しかし、クラスの付与/削除は JS で制御できます。このため、サイト上から任意のモードを設定できるようにするためには、クラスを指定する方式を使います。

なお、ライト/ダークモードの設定の保存には SessionStorage や LocalStorage が使われるのが一般的です(今回のツールで設定を LocalStorage に保存してます)。

以下の画像では LocalStorage に key が theme、 value が dark の文字列が保存されていることがわかります。

LocalStorage にダークモードの設定が保存されているところ

JS で LocalStorage の値を取得しているため、画面をリロードしてもダークモードが維持されます。

簡易まとめ

  • メディアクエリは OS の設定を参照している
  • クラスを使う場合は、サイト上から自由にライト/ダークモードを設定できる
    • ただし、ブラウザの SessionStorage / LocalStorage などに設定を保存する必要がある

Tailwind CSS でダークモードを実装する方法

ここでは Tailwind CSS の説明は省きます。もし Tailwind CSS を初めて知る方は 拙ブログの紹介記事 をぜひご覧ください。

ダークモード設定方法

Tailwind CSS は v2 からdark: というクラス名の接頭辞が追加されました。この接頭辞を有効にするためには、tailwind.config.js に以下の記述を追加します。

// tailwind.config.js
module.exports = {
  darkMode: 'media',  // メディアクエリ方式
  // ...
}

もしくは

// tailwind.config.js
module.exports = {
  darkMode: 'class',  // クラス方式
  // ...
}

これでダークモード用のdark:接頭辞が利用できます。

ダークモード対応のクラス名の指定

Tailwind CSS は Utility First の CSS フレームワークです。例えば以下のようなクラス名を指定してみます。

<p class="text-black">text</p>

Tailwind CSS にこの HTML ファイルを入力すると次の CSS が出力されます(実際の出力結果とプロパティが異なりますが、ここでは説明のためにcolorとしています)。

.text-black {
  color: #000000;
}

これで テキストが黒い色になります。

次に、ダークモードの時にテキストを白くしたければクラスdark:text-whiteを追加します。

<p class="text-black dark:text-white">text</p>

これでビルドすると以下のような CSS が出力されます。

.text-black {
    color: #000000;
}

.dark .dark\:text-white {
  color: #FFFFFF;
}

これが Tailwind CSS でのダークモード対応方法です。

あとは以下の対応をすれば、サイトをダークモード化できます。

  • Headless UI の Switch などを活用し、htmldarkクラスを付与したり除外するボタンを作る
  • ボタンなどのパーツや、Primary / Secondary カラーなどに応じて適切なダークモード用の配色を選択し、クラス名に設定する

React + Tailwind CSS でダークモードの切り替えをするボタンを実装する

Tailwind CSS の公式ドキュメント Dark Mode - Tailwind CSS を参照しつつ、React でダークモード対応のボタンを作成するコード例を紹介します。

ダークモードに切り替える Custom Hooks を作成する

まずはダークモードとライトモードを切り替える Hooks を作成します。

前述のように html タグにクラス名「dark」 を付与、あるいは削除することで切り替えに対応できました。それを useEffect 内のコードで表現していきます。

// useSimpleDarkMode.ts
import { useCallback, useEffect, useState } from 'react'

type UseSimpleDarkMode = (isDark?: boolean) => {
  isDarkMode: boolean
  toggle: (isDark?: boolean) => void
}

// dark mode と light mode を切り替える
export const useSimpleDarkMode: UseSimpleDarkMode = (isInitialDark = false) => {
  const [isDarkMode, toggleTheme] = useState<boolean>(isInitialDark)
  const toggle = useCallback((isDark?) => {
    if (typeof isDark === 'undefined') {
      toggleTheme((state) => !state)
      return
    }

    toggleTheme(isDark)
  }, [])

  useEffect(() => {
    if (isDarkMode) {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
  }, [isDarkMode])

  return { isDarkMode, toggle }
}

設定の永続化は別の Custom Hooks で対応します。useSimpleDarkModeにはクラス名の切り替えという責務のみ担当させるため、useDarkModeという命名はしていません。

useSimpleDarkMode をボタンに適用する

useSimpleDarkMode をボタンに適用します。これでダークモードの切り替えが可能です。

import { useSimpleDarkMode } from './useSimpleDarkMode'

const DarkModeButton: React.VFC = () => {
  const { isDarkMode, toggle } = useSimpleDarkMode()

  return (
    <Button onClick={() => toggle()}>
      {isDarkMode ? 'light' : 'dark'}
    </Button>
  )
}

export default DarkModeButton

Tailwind CSS の設定をpreview.jsで読み込みます。

// src/assets/style.css

@tailwind base;
@tailwind components;
@tailwind utilities;
// .stories/preview.js

import '../src/assets/style.css'

Storybook 上で表示します。

ライトモード ダークモード
ライトモードのボタン ダークモードのボタン

なお、上記画像に適用しているボタンのスタイルはコード内では省略しています。

設定を LocalStorage に保存し、再アクセス時に LocalStorage から読み込むための Custom Hooks を作成する

上記のままではダークモードの設定を永続化できないため、useSimpleDarkMode を利用する別の Hooks useDarkModeを作成します。

なお、LocalStorage へのアクセスには react-use の useLocalStorage を利用しています。

import { useSimpleDarkMode } from './useSimpleDarkMode'
import { useEffect } from 'react'
import { useLocalStorage } from 'react-use'

const Theme = {
  Dark: 'dark',
  Light: 'light',
} as const

type UseDarkMode = () => {
  isDarkMode: boolean
  toggle: (isDark: boolean) => void
}

export const useDarkMode: UseDarkMode = () => {
  const [value, setValue] = useLocalStorage<typeof Theme['Dark' | 'Light']>('theme')
  const { isDarkMode, toggle } = useSimpleDarkMode()

  const persistToggle = (isDark: boolean) => {
    toggle(isDark)
    setValue(isDark ? Theme.Dark : Theme.Light)
  }

  useEffect(() => {
    if (
      value === Theme.Dark ||
      (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
    ) {
      toggle(true)
      setValue(Theme.Dark)
    } else {
      toggle(false)
      setValue(Theme.Light)
    }
  }, [value, setValue, toggle])

  return { isDarkMode, toggle: persistToggle }
}

persistToggle をボタンのonClickに渡すことでダークモードの切り替えをしながら LocalStorage に設定を保存します。

useEffectではコンポーネントの初回レンダリング時にローカルストレージ、または OS の設定を読み込むようにしています。

実際にボタンに適用する際は、上記のコード例からuseSimpleDarkModeuseDarkModeに差し替えるだけです。

import { useDarkMode } from './useDarkMode'

const DarkModeButton: React.VFC = () => {
  const { isDarkMode, toggle } = useDarkMode()

  return (
    <Button onClick={() => toggle()}>
      {isDarkMode ? 'light' : 'dark'}
    </Button>
  )
}

export default DarkModeButton

これでサイト上でダークモード、ライトモードを切り替えるボタンを実装できました!

まとめ

Tailwind CSS でダークモードに対応する方法はとても簡単です。一方、ダークモード対応の配色を選ぶのは少々骨が折れます。

ただ、それも一度決めてしまえば使いまわせるので大変なのは一番最初くらいです。

私は addon-a11y を導入した Storybook 上で、アドオンの基準にパスするようなダークモード用の配色を決めていきました。

Storybook でダークモードのボタンコンポーネントを表示している

結果、Lighthouse で高得点を得ることができました。

もともと1ページだけでパーツ数も多くないからなのですが、テストと同じでいい点数を取ると嬉しいです!

この記事を読んでくださった方のダークモード実装に対する心理的なハードルを下げられたのであれば幸いです。

Happy hacking 🎉

参考

Discussion