CSSで、ダークモードに合わせて色を切り替える方法

​​CSS​HTML,Playwright,ダークモード

OS のダークモードに合わせてページの色を変えるには、色をカスタムプロパティにまとめ、@media (prefers-color-scheme: dark) の中で値を入れ替えます。あわせて :root に color-scheme: light dark を書くと、入力欄やボタンもダークの色になります。light-dark() に対応したブラウザ(Chrome 123 以降、Safari 17.5 以降など)だけを対象にできるなら、light-dark() でライトとダークの色を1行に並べて書けます。どれも JavaScript は要りません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。OS の設定は実際の OS の画面では変えず、Playwright の機能でブラウザに prefers-color-scheme の値を指定して切り替えました。色は getComputedStyle() で読み、ページの地の色はスクリーンショットの点の色で測りました。Firefox と Safari、スマートフォンの実機は試していません。選んだテーマを保存して次に開いたときにも使う仕組みや、ページを開いた瞬間の表示のちらつきも、この記事では扱いません。

色をカスタムプロパティにまとめ、メディアクエリで入れ替える

まず、ページで使う色を :root のカスタムプロパティにまとめます。ライトの色を基本にして、OS がダークのときだけ値を上書きします。

css
/* OS の設定に合わせて、カスタムプロパティの値を入れ替える */
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --text: #1f2a30;
  --card: #eefafd;
  --accent: #067a98;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #12181b;
    --text: #e3ecef;
    --card: #1e2b31;
    --accent: #5fd3ea;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--card);
  border-left: 4px solid var(--accent);
  padding: 12px 16px;
}

a {
  color: var(--accent);
}

色を使う側(body や .card)は var(--bg) のように名前で書くだけなので、ダーク用の色を足すときに直すのは :root の中だけです。MDN の prefers-color-scheme の説明によると、この値は OS かブラウザの設定から決まります。light には、ライトを選んだ場合だけでなく、特に何も選んでいない場合も含まれます。

カード、リンク、入力欄、ボタン、チェックボックスを並べたページにこの CSS を読み込み、それぞれの色を読みました。

要素 OS がライト OS がダーク
body の背景 rgb(255, 255, 255) rgb(18, 24, 27)
body の文字 rgb(31, 42, 48) rgb(227, 236, 239)
.card の背景 rgb(238, 250, 253) rgb(30, 43, 49)
リンク rgb(6, 122, 152) rgb(95, 211, 234)
入力欄の背景 rgb(255, 255, 255) rgb(59, 59, 59)
ボタンの背景 rgb(239, 239, 239) rgb(107, 107, 107)

ページを開いたあとに OS の設定をライトからダークに変えると、再読み込みしなくても body の背景は rgb(255, 255, 255) から rgb(18, 24, 27) に変わりました。

color-scheme を書かないと、入力欄が白いまま残る

上の表の入力欄とボタンには、色を何も指定していません。それでもダークの色になったのは、:root の color-scheme: light dark のおかげです。この1行を消した CSS も用意して、OS がダークのときの色を比べました。

上から3段の比較。1段目は OS がライトのときで、白い背景に水色のカード、白い入力欄。2段目は OS がダークで color-scheme: light dark を書いたとき。背景もカードも暗く、入力欄とボタンは暗い色に、チェックボックスもダーク用の色になっている。3段目は OS がダークで color-scheme を書かないとき。背景とカードは暗いが、入力欄とボタンは白っぽいまま、チェックボックスもライトのときと同じ色のまま残っている
color-scheme を書かないと、ページはダークでも入力欄やボタンはライトのままになる
OS がダークのとき color-scheme: light dark あり 書かない
body の背景 rgb(18, 24, 27) rgb(18, 24, 27)
入力欄の背景 rgb(59, 59, 59) rgb(255, 255, 255)
入力欄の文字 rgb(255, 255, 255) rgb(0, 0, 0)
ボタンの背景 rgb(107, 107, 107) rgb(239, 239, 239)

自分で指定した色はメディアクエリで変わりますが、ブラウザが用意している部品の色は変わりません。MDN の color-scheme の説明にあるとおり、color-scheme で変わるのは、ページの地(キャンバス)の色、スクロールバー、フォームの部品などの既定の色です。

色を何も指定していないページでも試しました。OS がダークのとき、ページの地の色は、何も書かないと #ffffff、color-scheme: light dark を書くと #121212 でした。文字の色も黒から白に変わります。HTML の <head> に <meta name="color-scheme" content="light dark"> を書いた場合も、CSS で書いた場合と同じ色になりました。MDN の meta の color-scheme の説明によると、meta はページ全体に、CSS の color-scheme は要素ごとに、使える配色を伝えます。

light-dark() なら、ライトとダークの色を並べて書ける

light-dark() を使うと、メディアクエリを書かずに、ライトの色とダークの色を1行に並べられます。

css
/* light-dark() で、ライトとダークの色を1行に並べて書く */
:root {
  color-scheme: light dark;
  --bg: light-dark(#ffffff, #12181b);
  --text: light-dark(#1f2a30, #e3ecef);
  --card: light-dark(#eefafd, #1e2b31);
  --accent: light-dark(#067a98, #5fd3ea);
}

body や .card の書き方は、メディアクエリの方法と同じです。読んだ色も、上の表とすべて同じになりました。

light-dark() はメディアクエリを見ているのではなく、その要素の color-scheme を見て色を選びます。MDN の light-dark() の説明では、:root に color-scheme: light dark を書いて使う方法が示されています。OS をダークにしたまま、:root の color-scheme の行を消したり、値を変えたりして確かめました。

:root の color-scheme body の背景 body の文字
light dark rgb(18, 24, 27) rgb(227, 236, 239)
行を消す(計算値は normal) rgb(255, 255, 255) rgb(31, 42, 48)
normal rgb(255, 255, 255) rgb(31, 42, 48)
light rgb(255, 255, 255) rgb(31, 42, 48)
dark rgb(18, 24, 27) rgb(227, 236, 239)
注意

light-dark() を使うときに color-scheme を書き忘れると、OS がダークでもライトの色のままになります。ダークの設定で一度開いて確かめてください。

light-dark() に渡せるのは、色か画像です。CSS.supports() で確かめると、color: light-dark(#fff, #000) は受け付けられ、width: light-dark(10px, 20px) のような長さは受け付けられませんでした。余白や線の太さをダークで変えたいときは、メディアクエリを使います。

ボタンで選んだテーマを OS の設定より優先する

light-dark() は color-scheme を見て色を選ぶので、ページの中に「ライト」「ダーク」のボタンを置いて、:root の color-scheme を切り替えるだけで、すべての色が変わります。CSS は、上の :root の後ろに次の2つを足します。

css
/* ボタンで選んだテーマを、OS の設定より優先する */
:root[data-theme="light"] {
  color-scheme: light;
}
:root[data-theme="dark"] {
  color-scheme: dark;
}

ボタンは、押されたら <html> の data-theme を書き換えます。「OS に合わせる」では data-theme を消して、:root の color-scheme: light dark に戻します。

html
<p>
  <button type="button" data-set="">OS に合わせる</button>
  <button type="button" data-set="light">ライト</button>
  <button type="button" data-set="dark">ダーク</button>
</p>
<script>
  for (const b of document.querySelectorAll('[data-set]')) {
    b.addEventListener('click', () => {
      const theme = b.dataset.set;
      if (theme) document.documentElement.dataset.theme = theme;
      else delete document.documentElement.dataset.theme;
    });
  }
</script>
OS がライトの状態で、ボタンを3回押す。1回目に「ダーク」を押すと、ページの背景も入力欄も暗くなり、下に出している data-theme が dark、body の背景色が rgb(18, 24, 27) に変わる。2回目に「ライト」を押すと明るく戻り、data-theme が light になる。3回目に「OS に合わせる」を押すと data-theme がなしになり、OS がライトなので明るいまま。変わった値は、変わった直後だけピンクになる
OS がライトのまま、ボタンで「ダーク」「ライト」「OS に合わせる」を順に押したときの色の変わり方

OS がライトのときとダークのときで、それぞれボタンを押して body の背景色を読みました。

押したボタン OS がライト OS がダーク
OS に合わせる rgb(255, 255, 255) rgb(18, 24, 27)
ライト rgb(255, 255, 255) rgb(255, 255, 255)
ダーク rgb(18, 24, 27) rgb(18, 24, 27)

入力欄の背景も、どの場合も body の背景に合わせて、ライトのときは rgb(255, 255, 255)、ダークのときは rgb(59, 59, 59) になりました。

一部だけダークにするときは、文字の色も指定し直す

color-scheme は要素ごとに付けられるので、ライトのページの中で、カードだけをダークにすることもできます。OS がライトのときに、.card に color-scheme: dark を足して色を読みました。

.card の中の色 メディアクエリの方法 light-dark() の方法
背景 rgb(238, 250, 253) rgb(30, 43, 49)
リンク rgb(6, 122, 152) rgb(95, 211, 234)
文字 rgb(31, 42, 48) rgb(31, 42, 48)

メディアクエリの方法では、OS の設定だけを見て色を決めているので、カードはライトの色のままでした。

light-dark() の方法では、カードの背景とリンクはダークの色になりました。ところが、カードの中の文字は、body で決まったライトの文字の色(rgb(31, 42, 48))のままでした。color は親から子へ受け継がれるプロパティです(MDN の color の説明)。カードには color を書いていないので、body の文字の色をそのまま受け継ぎます。暗い背景に暗い文字が重なって読みにくくなるので、ダークにする要素には color: var(--text) も書きます。こうすると、文字の色も rgb(227, 236, 239) になりました。

css
.card {
  color-scheme: dark;
  color: var(--text);
}

対応しているブラウザ

MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べた、最初に対応したバージョンです。

機能 Chrome Firefox Safari iOS の Safari
prefers-color-scheme 76 67 12.1 13
color-scheme プロパティ 81 96 13 13
<meta name="color-scheme"> 81 96 12.1 12.2
light-dark()(色) 123 120 17.5 17.5

light-dark() は、ほかの3つに比べて対応したバージョンが新しく、Safari では 17.5 からです。古いブラウザも対象にするなら、メディアクエリの方法にします。

​Posted by 86