ダークモードは色の反転ではない|自分のパレットで試したら2色とも落ちた

ダークモードは色の反転ではない|自分のパレットで試したら2色とも落ちた

ダークモード対応は「色を反転させる作業」だと思われがちですが、実際にやると反転できないものが次々に出てきます。

このブログはまだダークモードに対応していません。対応するとしたら何が壊れるのかを、実際にダーク面に置いて確かめました。以下の数値は、このサイトが使っている色をそのまま計算したものです。

暗い背景で影が消える・白背景の画像が浮く・黒い罫線が見えない・前景色がコントラスト基準を割る、という4つの問題と対処を並べた図
左が「そのまま持ってきた場合」、右が対処後
目次

前景色は作り直しになる

いちばん分かりやすいのがここです。ライトモードで使っている色を、そのまま暗い背景(#0e1526)に置いて測りました。

用途ライト用の色ダーク面での比判定
リンク#6d5ae63.69:1NG
補助テキスト#5a6b853.36:1NG
リンク(明るくした)#7d6ce94.50:1OK
補助テキスト(明るくした)#7d90a55.55:1OK

2つとも基準を割りました。これは特殊な例ではなく、ほぼ必ず起きます。ライトモードの前景色は「白の上で読める暗さ」を狙って選んでいるので、暗い背景に置けば当然コントラストが落ちます。

つまりダークモードのパレットは、ライトモードの色を反転させて作るものではなく、別に用意するものです。同じ「リンク色」という役割に対して、明るい面用と暗い面用の2つの値を持つことになります。

計算の仕方は配色を実測した記事と同じで、明度を少しずつ上げながら4.5:1を超える値を探しました。ただし基準ちょうどを狙わないほうがいいのも同じです。#7d6ce9 の4.50:1 は薄氷なので、実際にはもう一段明るくします。

「暗さ」で作っていたものが全部使えない

色の値以上に厄介なのがこちらです。ライトモードでは「暗くする」ことでいろいろな表現をしています。それがダーク面では全部無効になります。

影で重なりを表せない

box-shadow: 0 4px 12px rgba(0,0,0,.5) は、白い紙の上だから影に見えます。暗い背景の上では、黒を重ねても何も起きません。

ダークモードで「浮いている感じ」を出す方法は2つです。

  • 面を明るくする
    手前にあるものほど背景を明るくする。影の代わりに明度で階層を作る
  • 薄い枠線を足す
    rgba(255,255,255,.12) 程度の線で輪郭を出す

1つ目は、多くのデザインシステムが採用している考え方です。「奥=暗い、手前=明るい」というルールにすると、階層が何段あっても一貫します。影を残す場合も、より暗くするのではなく、影を弱めて枠線と併用する形になります。

罫線が消える

border-bottom: 1px solid rgba(0,0,0,.08) のような書き方は、ライトモードでは自然な区切り線ですが、ダークモードでは完全に見えなくなります。図の3番がその比較です。

直し方は簡単で、rgba(255,255,255,.12) に置き換えるだけです。問題はこういう半透明の黒が、コードのあちこちに直書きされていることのほうです。

白背景の画像が浮く

これがいちばん目立ちます。ロゴやスクリーンショットが、暗い画面の中で明るい四角として飛び出します。

CSSでどうにかするのは難しく、素材側の対応になります。

  • ロゴは透過にして、明色版を別に用意する
    <picture> で出し分ける
  • 図やグラフは、ダーク面用に描き直す
    背景を透明にしただけでは、黒い文字が読めなくなる
  • 写真はそのままでよい
    明るさを落とすと、かえって不自然になる

作業量として重いのはここです。画像の枚数だけ対応が必要になるので、記事が多いサイトほどコストが上がります。図をSVGで持っていれば、色をCSS変数にして切り替えられます。

夜の暗い部屋。デスクランプ1つだけが木のテーブルとマグカップを照らしている写真
暗いところでは、明るさの差の付け方が変わる

CSSの持ち方

実装で決定的に効くのが、変数の名前の付け方です。

/* 悪い例: 色そのものを名前にしている */
--navy: #0e1526;
--light-gray: #f3f6fa;
/* → ダークモードで「--light-gray が濃い色になる」という事態になる */

/* 良い例: 役割を名前にする */
--bg:          #f3f6fa;   /* ページの地 */
--surface:     #ffffff;   /* カードの面 */
--text:        #0e1526;   /* 本文 */
--text-muted:  #5a6b85;   /* 補助 */
--link:        #6d5ae6;
--border:      rgba(0,0,0,.10);
@media (prefers-color-scheme: dark) {
  :root {
    --bg:          #0e1526;
    --surface:     #16203a;   /* 手前ほど明るく */
    --text:        #e8eef5;   /* 純白にしない */
    --text-muted:  #7d90a5;   /* 明るくした値 */
    --link:        #7d6ce9;   /* 明るくした値 */
    --border:      rgba(255,255,255,.12);
  }
}

役割で名前を付けておけば、差し替えるのは :root の中だけで済みます。逆に色名で持っていると、全部のCSSを書き換えることになります。

ダークモード対応の実作業のほとんどは、色を決めることではなく「直書きされた色を変数に置き換えること」です。あとから対応する場合、ここがいちばん時間を食います。

本文を純白にしない

細かい点ですが、ダーク面の本文に #ffffff を使うと、コントラストが強すぎて目が疲れます。実測すると18.20:1で、基準の4倍です。

#e8eef5 なら15.58:1 で、十分読めてまぶしくありません。基準を満たすことと、読みやすいことは別という例です。

あとから入れるときの順番

すでに動いているサイトに後付けする場合、いきなり全部を対応しようとすると終わりません。効果と手間の比で並べるとこうなります。

  1. 直書きの色を洗い出す
    # や rgb( でCSSを検索して、変数に置き換える。ここが全体の8割の作業量
  2. 背景・面・文字・罫線の4つだけ先に切り替える
    この4つでページの大半は成立する
  3. コントラストを測って前景色を作る
    反転では足りないことを、数値で確認する
  4. 画像を洗い出す
    白背景のものだけリスト化する。数が多ければ段階的に
  5. 細部を詰める
    コードハイライト、グラフ、フォーム部品、スクロールバー

見落としやすいのが5番目のコードハイライトです。多くのライブラリはライト用とダーク用のテーマを別々に持っているので、こちらも切り替える必要があります。ダークモードの画面に、白いコードブロックだけが残るのはよくある取りこぼしです。

もう1つ、OGP画像はダークモードの影響を受けません。SNSに出るのは常に同じ画像なので、そこは切り替え対象外です。逆に、サイトがダークでもOGP画像は明るいまま、という不整合は気にしなくて構いません。

対応しない、という判断もある

ここまで書いておいてなんですが、やらないのも選択肢です。

コストの大半は色を決めることではなく、画像の作り直しと、以後すべての実装で2パターンを確認し続けることです。1人で運用しているサイトで画像が数百枚あるなら、その手間に見合うかは真面目に検討する価値があります。

中途半端に対応して、一部の画面だけ白く残る状態がいちばん体験として悪いので、やるなら最後までやる、やらないなら明るいまま整えておく、という判断になると思います。

切り替えをどう提供するか

OSの設定に従うだけなら prefers-color-scheme で完結しますが、手動で切り替えたいユーザーは必ずいます。「OSはダークだが、このサイトはライトで読みたい」という要望です。

選択肢は3つ用意するのが親切です。

  • OSに合わせる(既定)
  • 常にライト
  • 常にダーク

実装では、html 要素に data-theme="dark" のような属性を付けて、メディアクエリと属性の両方で変数を定義します。属性のほうを後に書けば、手動の選択が優先されます。

注意点が1つあります。選択の反映が遅れると、一瞬だけ白い画面が出ます。ページ読み込み後にJavaScriptで属性を付ける実装だと必ず起きるので、head 内の同期スクリプトで、描画前に属性を付ける必要があります。

あわせて <meta name="color-scheme" content="light dark"> を入れておくと、スクロールバーやフォーム部品もダーク側の見た目になります。これがないと、フォームだけ白く残ります。

まとめ

  • 前景色は作り直しになる
    このサイトの色は2つとも、ダーク面では基準を割った
  • 影は使えない
    手前ほど面を明るくして階層を作る
  • 黒の半透明は全滅する
    罫線もオーバーレイも白の半透明に置き換える
  • 白背景の画像がいちばん重い
    枚数だけ作業が必要になる
  • 変数は色名ではなく役割で命名する
    実作業の大半は直書きの置き換え
  • 本文を純白にしない
    基準を満たすことと読みやすさは別
  • 手動切り替えを用意し、描画前に反映する
    一瞬の白い画面を出さない

ダークモードは「色を2セット持つ機能」ではなく、暗さで作っていた表現を、明るさで作り直す作業だと考えると見積もりを外しにくいと思います。

次に読む記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

わどこんのアバター わどこん

実務12年のバックエンド・インフラエンジニア。バックエンド開発からクラウド・インフラの設計・構築・運用まで担当しています。主要言語は Java・Kotlin・PHP・Python。運用の現場で拾った知見を、再現できる手順に落として残すのがこのブログのテーマです。

目次