💡

Webサイトのダークモード対応

に公開

個人サイトshetommy.comをダークモード対応しました。

いい感じですね。
Webフロントは本業じゃないので大変でした。
忘れないうちにやったことを書き残しておきます。

詳細

詳細見たい方はこちら。

https://github.com/0si43/shetommy.com/pull/91

shetommy.comのざっくり改善ロードマップ

このサイトも長いもので、2021年に公開して、片手間で改善していっています。
ロードマップをざっくりまとめると、こんな感じになりました。

  • 個人ブログ機能
  • ブログの画像対応
  • TypeScript化
  • OGP対応(自サイト)
  • ブログの目次生成
  • ブログのOGP対応(他サイト)
  • ブログのネストされたリスト
  • ダークモード対応←new!
  • ブログの画像サイズをいい感じにしたい
  • ブログにTwitterとかYouTubeとか埋め込みたい

並べてみるとけっこう色々やりましたね。
画像は今固定長なんですけど、これをアス比保ったまま表示にして、タップすると拡大みたいな仕様にしたい。
これはたぶんやればできる。
ブログのembedは、実はYouTubeはできていて、Twitterも実はやればできるような気がしています。
ただ主要サービス全部対応するとなると修羅。

書いてないけども、Next.jsのアップデート対応もそういえばしないといけないんですけど、さすがに腰が重い状態です。

Claude Codeの実装

最初Claude Codeに丸投げして、ダークモード実装してもらったら、動くやつができまして。

https://github.com/0si43/shetommy.com/pull/82

SVG画像周りとかだけ対応できてませんでしたけど、そこはこっちで対応すれば、最低限使えるレベルではありました。
でもちょっと個人的に気に入らないところがありまして。

ボタンのUIとか、OS設定の扱いとかが気に入らず、
Claude Codeに書き直してもらおうとしても、Webの知識なさすぎて上手く指示出せなくて、結局自分で書こうとなりました。

ダークモード対応の仕様決め

ダークモード対応ですが、ライトモード/ダークモードの2つの状態だけを最初考えていたのですが、
よくよく詰めていくと「OS設定に従う」(auto, system)という第3の状態があることに気づきました。
最大でauto/light/darkの3つの状態が存在します。
(カラースキームはライト/ダークの2種類)

Claude Codeの実装はReactのuseContextを使うものですが、OS設定を見ようとなるとやりづらいようです。
autoをどう実現するかですが、CSSに便利な prefers-color-scheme というのが用意されていました。

https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme

これを使うことにしました。

右上のボタンでauto/light/darkを切り替えられることにしました。
ボタンを押すとローカルストレージのpreferAppearanceにそれぞれ文字列をつっこみます。
文字列がなかったとき(初回アクセスなど)はOS標準を見ます。
'auto'の場合は、常にnullという仕様にしても良かったんですが、一応詰めておいた方が丁寧かなと思ってそうしました。

以上をまとめると、CSS側の条件分岐はこうなります。

  • LocalStorage(preferAppearance): 'auto' or null -> prefers-color-scheme
  • LocalStorage(preferAppearance): 'light' -> light theme
  • LocalStorage(preferAppearance): 'dark' -> dark theme

ダークモードのあるべき仕様

「最大でauto/light/darkの3つの状態」と書きましたが、どこまでユーザーに設定させるかは思想が出るのかと思います。

まずautoだけで良い、という考えがあります。
OS設定にアプリは素直に従う。
なぜならユーザーがダークモードにしているのだから、当然ダークモードで表示すべき。
嫌ならOS設定を変えてください。
Appleの純正アプリはこの思想でつくられています。

次にautoなしで、light/darkの2択を選ばせる、もあると思います。
ユーザー体験は下がりますけど、実装的には楽になるかと。

僕自身はAppleの思想と同じく、OS設定に従ってればいいと思ってたんですが、
個人開発で出してるiOSアプリの要望に「OS設定とは別でダークモード設定させてくれ」というのがちょこちょこ来てまして、一定のニーズがあるみたいです。

コードの修正

まずボタンをつくります。
当初はライト・ダークの2つを切り替えるスイッチにしようと思ってたんですが、選択肢が3つになったので、タップしたら次に切り替わるだけのシンプルなボタンにしました。

Zennも同じタイミングでダークモード対応していたのですが、ボタン押すとドロップダウンメニュー的に表示されるデザインで、面白いなと思いました。

タップしたら切り替わるボタン、本当はあんまりUI/UXとして良くないんですよね。
これはどっかで考察したい。
ユーザーがボタン押したタイミングで、ローカルストレージに文字列をつっこみます。

localStorage.setItem('preferAppearance', Appearance.Light);

ただCSSはローカルストレージを直接読むことができません。
なのでCSSが読めるように、HTMLを弄る必要があります。

document.documentElement.setAttribute('site-appearance', Appearance.Light);

これによって、HTML(DOMと言ったほうがいい?)が書き変わるようです。

<html lang="ja" site-appearance="light">
  <head>...</head>
  <body>...</body>
</html>

CSSの対応

実はコードそのものの変更はそんなになく、後はCSSをひたすら修正するだけです。
カラー定義ファイルがこちらです。

:root {
  --primary-text-color: black;
  --background-color: white;
}

/* ダークモード: ユーザー設定 */
[site-appearance="dark"] {
  --primary-text-color: white;
  --background-color: black;
}

/* ダークモード: OS設定 */
/* CSS内の条件分岐の都合上書いているが、設定が重複しているので消したい */
:root:is([site-appearance="auto"], :not([site-appearance])) {
  @media (prefers-color-scheme: dark) {
    /* text color */
    --primary-text-color: white;
    --background-color: black;
  }
}

これをglobals.cssに読ませると、各CSSモジュールで使うことができます。
本当はCSS上ではライトとダークの2定義をまとめたかったんですが、どうしてもダークモードで二重で書かなければダメでした。
ここなんとかする方法知ってる方は教えてください。

色を呼び出す側はこのような記述で呼ぶことになります。

html,
body {
  color: var(--primary-text-color);
  background-color: var(--background-color);
}

CSS内の色定義をすべて変数経由にしたら、ダークモード対応完了です。

SVGファイル

一個書き忘れてました。
SVGファイルなんですが、CSSで色を変えられるものとそうでないものがあることに気づきました。

color: var(--primary-text-color);

変えられるファイルはこれで変わるので、簡単にダークモード対応できます。
変わらないファイルを調べてみると、どうやらファイルとして持っているSVGがダメでした。

インラインSVGという形式にすると、CSSが効くことを知りました。
つまりこういう指定です。

const Zenn = () => (
  <svg height={28} width={28} viewBox="-2 -2 28 28">
    <path d="M.264 23.771h4.984c.264 0 .498-.147.645-.352L19.614.874c.176-.293-.029-.645-.381-.645h-4.72c-.235 0-.44.117-.557.323L.03 23.361c-.088.176.029.41.234.41zM17.445 23.419l6.479-10.408c.205-.323-.029-.733-.41-.733h-4.691c-.176 0-.352.088-.44.235l-6.655 10.643c-.176.264.029.616.352.616h4.779c.234-.001.468-.118.586-.353z" />
  </svg>
)

export default Zenn

僕の場合、何かをパクったときに半分インラインSVG、半分SVGファイルという中途半端な状態になっていたので、今回を期にSVGファイルはインライン形式にしました。

終わりに

以上が対応内容でした。
やること自体は思ったより簡単でした。
Next.jsの知識というよりはひたすらCSSと向き合うことになりました。
CSSってすごいなと思いました。

なんだかんだで時間かかったのは、知識不足と、元々のCSSが内容理解しないまま人のをパクったものだったので、良くないところを直しながら進めることになったからかと思います。
あと色変更の検証が目grepになるので、そこでも時間食いますね。
CSSのカラーエイリアスを使うというポリシーがあったので、しっくり来る色がないときはだいぶ探しました。

ダークモード対応、大した規模じゃない個人サイトでもこれだけしんどいんだとすると、productionで動いてるWebサービスだと相当しんどいんでしょうね。
CSSがキレイに保たれてればいいですけど、たぶんそうでない現場も多いと思うので……

(了)

Discussion