Nuxt.js + Content Module のブログをダークモード対応した

「MTG中とかでカメラオンにしてると、自分が見てるページがダークモード対応してるか否かで露骨に自分の顔の明るさが変わるのめっちゃ嫌」というツイートをしたものの、

こう言いながら自分のブログ対応していないのまずいのでは。と思い急遽。

要件

ダークモードの対応といえば色々と手法があるが、今回のゴールはこのようにしました。

  • ダークモードで閲覧したい人がダークモードで閲覧できる状態となる
  • OS の設定を見て自動でダークモードにすることはしない
  • 能動的にモードを切り替えた場合、切り替えた状態は保持されて、次以降のアクセス時は望んだモードで表示される

自動でダークモードにしないのは、読み物は白背景に黒文字のほうが読みやすい人のほうがマス層と感じたため。あくまでも夜や他がダークモードだらけなど、白が眩しいときに使える状態を目指します。

改修内容

このブログは現状 Nuxt(v2) + Content Module で運用しているため、以下の手順を実施しました。

  1. @nuxtjs/tailwindcss のバージョンをアップグレード
  2. @nuxtjs/color-mode をインストール
  3. 機械的に置き換えられない部分のスタイル調整
  4. 能動的に切り替えるためのボタンを設置

1. @nuxtjs/tailwindcss のアップグレード

https://github.com/nuxt-community/tailwindcss-module

特に複雑なことはせずにアップグレードを実施。DarkMode 非対応のバージョンのまま上げていなかったため、 Tailwind が 1.x から 2.0 に。Breaking Changes を含むため、このタイミングで bg-blue-600 の色が変わる。

これまでは#3182ce だったが、#2563eb になってしまった。かなり彩度が上がってしまい意図しないほど主張が強くなってしまっているため、多少を色を調整。

あとはほとんど変化なく終了。JIT はついでに有効にしました。

2. @nuxtjs/color-mode をインストール

https://github.com/nuxt-community/color-mode-module

カラースキームの変更について自分で実装しても良いのですが、 light <-> dark 間の切り替えはある程度シビアに実装しないとチラツキがストレスになります。今回は特に大きなコストをかけたいわけではないため公認のライブラリに移譲することにします。

yarn add で入るのは既に Nuxt v3 向けのものとなってしまっているため、 Nuxt v2 向けの最新である 2.1.0 をインストール。 tailwind.dark の class 名を要求するため、設定を少し書き換える必要があります。

// nuxt.config.ts
export default {
  // ...
  colorMode: {
    classSuffix: '',
  },
}

こうすることで @nuxtjs/color-mode 側が root 要素に対して .dark を付与するようになるため、このタイミングで tailwind.config.js 側も対応させます。

module.exports = {
  darkMode: 'class'
}

darkMode オプションに media | class を付与することでダークモードが有効になる。media モードで追加すると CSS 側の Media Query で判断されてしまい、 JavaScript 側から能動的に切り替えられないため今回 class モードを採用しました。

3. 機械的に置き換えられない部分のスタイル調整

ページ自体の見た目に関するところは勿論、 syntax highlight や markdown などのコードから生成される部分はそれなりに手書きする必要が出てきます。今回の場合、 syntax highlight については Content Module が 1 つのカラースキームしか定義できないため、手動での CSS 追加で対応しました。

といっても、Prism の公式リポジトリから好みのスタイルを選択し、.dark .markdown-body .nuxt-content-highlight 配下に適用するだけで完了します。

今回は普段のコーディングでも利用している Atom One Dark を採用しました。

4. 能動的に切り替えるためのボタンを設置

ここまでで一通り準備は完了したので、最後に切り替えボタンを実装する。今回は素朴にボタンの押下時に反応する形とします。

@nuxtjs/color-mode では、localStorage の nuxt-color-mode 内にデータがある場合その設定を尊重するように作られているため、押下処理にて併せてアップデートします。

<template>
  <div>
    <button type="button" @click="handleClickChangeColorScheme(currentColor === 'light' ? 'dark' : 'light')">
      <img v-if="currentColor === 'light'" src="~/assets/images/sun.svg" width="20" height="20" alt="current: light mode">
      <img v-if="currentColor === 'dark'" src="~/assets/images/moon.svg" width="20" height="20" alt="current: dark mode">
    </button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  computed: {
    currentColor() {
      return this.$colorMode.value
    }
  },
  methods: {
    handleClickChangeColorScheme(next: 'light' | 'dark') {
      if (!(['light', 'dark'].includes(next))) {
        return
      }
      this.$colorMode.preference = next
      localStorage.setItem('nuxt-color-mode', next)
    }
  }
})
</script>

ここまでできたら切り替えた場合の表示を自由自在に検証できるようになるため、 @nuxtjs/color-mode が提供している OS の設定に合わせる機能を無効化します。

// nuxt.config.ts
export default {
  // ...
  colorMode: {
    classSuffix: '',
    preference: 'light'
  },
}

最後に気になるところなどがあれば微調整して終わり。実際に手を施したところでいうと、この記事を書いている中で `some` の形式で記述する場所のハイライトが気になったので修正などを行いました。

これで一旦ダークモード化は完了。画像もダークモード化できるとベストですが、如何せんコストがかかりすぎるため、ここで終了としました。

おわりに

最近は専ら記事を書くだけでブログのシステム本体のアップデートをサボっていたため、意外といろんなパッケージを上げることになりました。

暫く放置気味だった原因として、 Nuxt v3 がいつ正式リリースされるのかわからない一方で、正式リリースされると Content Module は Docus へと置き換わることが規定事項であることがあったりもします。

そのため、本記事自体あまり寿命が長いわけではないのですが、意外と Tailwind がオフィシャルで Dark Mode 機能が提供されて以降の Nuxt + Tailwind でダークモード適用記事がなかったのでまとめてみました。

MTG中に自分の顔が明るくなったり暗くなったりしてストレスを感じる人が一人でも減ると幸いです。

(私自身はカメラオンのためには1時間以上かかる準備をしないと耐えられないタイプなので、オンにすることはほとんどないのですが)