CSSで、ダークモードに合わせて色を切り替える方法
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 がダークのときだけ値を上書きします。
/* 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 がダークのときの色を比べました。

| 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行に並べられます。
/* 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つを足します。
/* ボタンで選んだテーマを、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 に戻します。
<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 がライトのときとダークのときで、それぞれボタンを押して 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) になりました。
.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 からです。古いブラウザも対象にするなら、メディアクエリの方法にします。





