ダークモードにCSSで対応する|prefers-color-scheme・color-scheme・light-dark()と切りかえボタン、コピペ4種類

ダークモードにCSSで対応する。ライトとダークのカードの見本とコード

スマホやパソコンを「ダークモード」(画面全体を暗い色にする表示)にしている人は少なくありません。ところが、多くのサイトは白い背景のまま表示されるので、暗い画面の中で、そのページだけがまぶしく光ります。CSSには、見る人の端末の設定を読み取って、色を切りかえる仕組みがあります。

使いどころ

色をCSSの変数でまとめて管理しているサイトなら対応する。色があちこちに直接書かれていて、ロゴや写真も白い背景が前提のサイトは、中途半端な暗さになりやすいので、無理に対応しない。

今回は、色を変数にまとめて切りかえる基本の形、入力欄などブラウザが描く部品まで暗くする color-scheme、1行で2色を書き分ける light-dark()、ページの中のボタンで切りかえる形の4種類を、コピペ用のコードつきで紹介します。暗い表示で色を選ぶときの目安と、やりすぎないための判断もまとめました。

トミー

連載「WEBの技術」の30回目です。
今回は「ダークモード」。見た目を変える技術というより、「色をどう管理しているか」が問われる技術です。

完成見本:同じHTMLを、CSSだけで暗くする

いちばん上の見本は、ページの中のボタンで切りかえる形です。どう変わるかが分かるように、見本では「明るい→暗い」を自動でくり返しています。その下の3つは、同じ部品を「ライト」(明るい表示)と「ダーク」(暗い表示)で並べました。左右とも、HTMLとCSSはまったく同じで、変わっているのは色の設定だけです。

THEME TOGGLE4. ボタンで切りかえる(設定を覚えておく)読む人が自分で明るい・暗いを選べるようにしたいとき
PREFERS-COLOR-SCHEME1. 色を変数にまとめて、ダークのときだけ入れかえるいちばん基本の形。どのサイトでもまずここから
COLOR-SCHEME2. 入力欄やチェックも暗くする(color-scheme)お問い合わせフォームなど、ブラウザが描く部品があるページ
LIGHT-DARK()3. 1行で2色を書き分ける(light-dark())ボタンなど、色を変えたい場所が少ないとき

※ 見本は読み上げソフトでは読まれないようにしています。端末で動きを減らす設定にしている場合は、いちばん上の見本の切りかえが止まります。

コピペ用のコード

CSSは、サイトのCSSファイルや、WordPressならテーマの「追加CSS」などに入れます。クラス名と変数名の頭の kd-dm は、サイトのほかのデザインと名前がぶつからないようにするための目印です。

1. 色を変数にまとめて、ダークのときだけ入れかえる

色を --kd-dm-text のような「変数」(CSSの中で使い回せる、名前を付けた値)にまとめておき、@media (prefers-color-scheme: dark)(端末がダークモードのときだけ使う指定)の中で、その変数の中身だけを暗い表示用に入れかえます。カードのCSSには色を直接書かず、変数で書いているので、カードの側は1行も変えずに済みます。

HTML
<div class="kd-dm-card">
  <p class="kd-dm-ttl">年末年始の営業について</p>
  <p class="kd-dm-txt">12月28日から1月4日まではお休みです。</p>
  <a class="kd-dm-link" href="/news/">詳しく見る →</a>
</div>
CSS
/* ふだん(明るい表示)の色 */
:root {
  --kd-dm-bg: #FFFFFF;
  --kd-dm-card: #F4F7FA;
  --kd-dm-text: #10283A;
  --kd-dm-sub: #4A5E6E;
  --kd-dm-accent: #0369A1;
  --kd-dm-line: #C9D3DB;
}
/* 端末がダークモードのときだけ、色を入れかえる */
@media (prefers-color-scheme: dark) {
  :root {
    --kd-dm-bg: #0F1B26;
    --kd-dm-card: #172634;
    --kd-dm-text: #E6EEF4;
    --kd-dm-sub: #A9B8C4;
    --kd-dm-accent: #7DD3FC;
    --kd-dm-line: #2E4254;
  }
}
body {
  background: var(--kd-dm-bg);
  color: var(--kd-dm-text);
}
.kd-dm-card {
  max-width: 320px;
  padding: 18px 22px;
  border: 1px solid var(--kd-dm-line);
  border-radius: 14px;
  background: var(--kd-dm-card);
  color: var(--kd-dm-text);
}
.kd-dm-ttl {
  margin: 0 0 4px;
  font-size: 16px;
  font-weight: 700;
}
.kd-dm-txt {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--kd-dm-sub);
}
.kd-dm-link {
  font-size: 14px;
  font-weight: 700;
  color: var(--kd-dm-accent);
}

2. 入力欄やチェックも暗くする(color-scheme)

入力欄・チェックボックス・スクロールバーは、ブラウザが描いている部品です。ページの色を暗くしても、これらは白いまま残ることがあります。color-scheme: light dark(このページは明るい表示と暗い表示の両方に対応している、とブラウザに伝える指定)を書くと、ブラウザが部品の色も暗い表示に合わせます。下のフォームは、色を1つも自分で決めていません。

HTML
<form class="kd-dm-form">
  <label for="kd-dm-mail">メールアドレス</label>
  <input id="kd-dm-mail" type="email" placeholder="you@example.com">
  <label><input type="checkbox" checked> お知らせを受け取る</label>
</form>
CSS
:root {
  /* 明るい表示と暗い表示の両方に対応している、とブラウザに伝える */
  color-scheme: light dark;
}
.kd-dm-form {
  max-width: 320px;
  padding: 18px 20px;
  border-radius: 14px;
  /* Canvas・CanvasText は、ブラウザが決める「背景」と「文字」の色 */
  background: Canvas;
  color: CanvasText;
}
.kd-dm-form label {
  display: block;
  font-size: 14px;
  font-weight: 700;
}
.kd-dm-form input[type="email"] {
  box-sizing: border-box;
  width: 100%;
  margin: 6px 0 14px;
  font-size: 16px;
}

3. 1行で2色を書き分ける(light-dark())

light-dark(明るいときの色, 暗いときの色) と書くと、ブラウザが今の表示に合わせて、どちらかの色を使います。@media で囲まなくてよいので、ボタン1つだけ色を変えたいときに短く書けます。使うには、2の color-scheme: light dark も必要です。比較的新しい書き方なので、使えないブラウザ用の色を先に1行書いておきます。

HTML
<a class="kd-dm-btn" href="/contact/">お問い合わせ</a>
CSS
:root {
  color-scheme: light dark;
}
.kd-dm-btn {
  display: inline-block;
  padding: 12px 24px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 700;
  text-decoration: none;
  /* light-dark() が使えない古いブラウザ用の色 */
  background: #0369A1;
  color: #FFFFFF;
  /* 明るい表示のときは左、暗い表示のときは右の色 */
  background: light-dark(#0369A1, #7DD3FC);
  color: light-dark(#FFFFFF, #0F1B26);
}
.kd-dm-btn:focus-visible {
  outline: 3px solid light-dark(#0369A1, #7DD3FC);
  outline-offset: 3px;
}

4. ボタンで切りかえる(設定を覚えておく)

端末の設定に合わせるだけでなく、ページの中のボタンでも切りかえられる形です。選んだ表示は html 要素の data-theme に書き、CSSでは :root[data-theme="dark"] のときに1の変数を入れかえます。ボタンの状態は aria-pressed(押されている/いないを読み上げソフトに伝える属性)で持たせます。選んだ表示は localStorage(ブラウザに小さなデータを残しておく仕組み)に残し、次に開いたときも同じ表示にします。1のCSSといっしょに使います。

HTML
<button class="kd-dm-toggle" type="button" aria-pressed="false">
  <span class="kd-dm-knob" aria-hidden="true"></span>
  ダークモード
</button>
CSS
/* ボタンで選んだ表示を、端末の設定より優先する */
:root[data-theme="light"] {
  color-scheme: light;
  --kd-dm-bg: #FFFFFF;
  --kd-dm-card: #F4F7FA;
  --kd-dm-text: #10283A;
  --kd-dm-sub: #4A5E6E;
  --kd-dm-accent: #0369A1;
  --kd-dm-line: #C9D3DB;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --kd-dm-bg: #0F1B26;
  --kd-dm-card: #172634;
  --kd-dm-text: #E6EEF4;
  --kd-dm-sub: #A9B8C4;
  --kd-dm-accent: #7DD3FC;
  --kd-dm-line: #2E4254;
}
.kd-dm-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 6px 6px;
  border: 1px solid var(--kd-dm-line);
  border-radius: 999px;
  background: var(--kd-dm-card);
  color: var(--kd-dm-text);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.kd-dm-knob {
  position: relative;
  width: 40px;
  height: 22px;
  border-radius: 999px;
  background: var(--kd-dm-line);
}
.kd-dm-knob::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--kd-dm-text);
  transition: transform .2s ease;
}
/* 押されている(ダーク)ときは、丸を右へ */
.kd-dm-toggle[aria-pressed="true"] .kd-dm-knob::after {
  transform: translateX(18px);
}
.kd-dm-toggle:focus-visible {
  outline: 3px solid var(--kd-dm-accent);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .kd-dm-knob::after {
    transition: none;
  }
}
JavaScript(</body> の直前に)
const kdBtn = document.querySelector('.kd-dm-toggle');
const kdOsDark = matchMedia('(prefers-color-scheme: dark)').matches;
// 前に選んだ表示があればそれを、なければ端末の設定を使う
let kdTheme = localStorage.getItem('kd-theme') || (kdOsDark ? 'dark' : 'light');

function kdApply(theme) {
  document.documentElement.dataset.theme = theme;
  kdBtn.setAttribute('aria-pressed', theme === 'dark');
}
kdApply(kdTheme);

kdBtn.addEventListener('click', () => {
  kdTheme = kdTheme === 'dark' ? 'light' : 'dark';
  kdApply(kdTheme);
  localStorage.setItem('kd-theme', kdTheme);
});

2と3の color-scheme は、HTMLの <head> の中に次の1行を書いても、ブラウザに伝えられます。CSSより先に読まれるので、ページを開いた瞬間の白い画面を減らせます。

HTML(head の中)
<meta name="color-scheme" content="light dark">

仕組み:端末の設定を読む、色の名前を入れかえる

prefers-color-scheme は「見る人が選んだ表示」を読む

@media (prefers-color-scheme: dark) は、見る人がOSやブラウザの設定で暗い表示を選んでいるときだけ、中のCSSを使う指定です。MDNでは、2020年1月から主要なブラウザで使えると説明されています。サイトの側から「暗くしなさい」と決めるのではなく、見る人の設定に合わせる、というのが基本の考え方です。

色を変数にしておくと、入れかえは1か所で済む

1では、:root(ページ全体のいちばん外側)に --kd-dm-text: #10283A; のような変数を並べ、カードの側は color: var(--kd-dm-text); のように変数の名前で色を使っています。ダークモードのときは、変数の中身だけを書きかえます。カードやボタンが何十個あっても、書きかえるのは色の表の1か所です。

反対に、color: #10283A; のように色をそのまま書いた場所がCSSのあちこちにあると、ダークモード用に、その全部をもう一度書くことになります。「使いどころ」で、色を変数で管理しているかどうかを分かれ目にしたのは、このためです。

color-scheme は「ブラウザが描く部分」に伝える

MDNによると、color-scheme を指定すると、ブラウザはページの地の色、スクロールバー、入力欄やチェックボックスなどの部品の色を、選ばれた表示に合わせます。一方で、作り手が色を決めた部分は変わりません。そこは prefers-color-scheme で自分で決める、という役割分担です。2のフォームでは Canvas・CanvasText(ブラウザが決める背景と文字の色の名前)を使っているので、色を1つも書かずに暗くなっています。

light-dark() は、color-scheme に合わせて2色から選ぶ

light-dark(#0369A1, #7DD3FC) は、その場所の color-scheme が明るい表示なら左、暗い表示なら右の色になります。@media を書かなくてよいぶん短く書けますが、color-scheme: light dark が指定されていないと、いつも左の色になります。MDNでは2024年5月から最新のブラウザで使えるとされていて、古い端末では使えないことがあるため、3では先に普通の色を1行書いています。使えないブラウザは light-dark() の行を読み飛ばし、先に書いた色で表示します。

ボタンで選んだ表示は、端末の設定より優先する

4では、ボタンを押すと html 要素に data-theme="dark" が付きます。CSSの :root[data-theme="dark"] は、1の :root より強い指定なので、端末の設定がどちらでも、ボタンで選んだ表示が勝ちます。選んだ表示は localStorage に残るので、次に開いたときも同じ表示になります。まだ一度も押していない人には、matchMedia()(JavaScriptから画面や設定の条件を調べる仕組み)で端末の設定を読んで合わせます。

色を変えるとどうなるか

ダークモードは「白と黒を入れかえる」だけでは、きれいに見えません。暗い背景の上での色の選び方を、同じカードで比べます。

背景の暗さ

真っ黒:カードとの差が強い
青みの暗い色:目安
明るめの灰色:暗く見えない

真っ黒(左)は、カードやボタンとの差が強く出て、文字が浮いて見えます。明るめの灰色(右)は、ダークモードにしている人から見ると、暗くした意味が薄れます。KAKERUでは、真っ黒ではなく、サイトの色(ここでは紺)を少し混ぜた暗い色を背景にしています。

文字の白さ

真っ白:まぶしく感じやすい
少し落とした白:目安
暗すぎる:読みにくい

暗い背景の上の真っ白な文字(左)は、明るさの差が約17対1と大きく、長い文章ではまぶしく感じる人がいます。少し灰色に寄せた白(中央)でも、背景との差は13対1以上あります。暗くしすぎた文字(右)は約3.3対1で、W3Cの基準が本文に求める4.5対1に届きません。

リンクやボタンの色

明るい表示の色のまま:2.9対1
明るくした色:9.2対1

明るい表示で使っていた濃い青(左)を暗い背景にそのまま置くと、明るさの差は約2.9対1しかなく、リンクが沈みます。暗い表示では、同じ青の仲間で明るい色(右、約9.2対1)に置きかえます。ブランドの色も、暗い表示用に1段明るい色を決めておく必要があります。

変えるところ明るい表示の値暗い表示の値気をつけること
ページの背景#FFFFFF#0F1B26真っ黒より、サイトの色を混ぜた暗い色
カードの背景#F4F7FA#172634ページの背景より少し明るくして、重なりを見せる
文字#10283A#E6EEF4真っ白より、少し落とした白
リンク・強調#0369A1#7DD3FC同じ色のままだと沈む。1段明るい色に
線#C9D3DB#2E4254影が見えにくいぶん、線で区切る

スマホと、見る人への配慮

明るさの差は、ライトとダークの両方で測る

今回の見本は、明るい表示で文字とカードの差が約14対1、小さな文字が約6.3対1、リンクが約5.5対1です。暗い表示では、文字が約13対1、小さな文字が約7.6対1、リンクが約9.2対1です。ダークモードを足すと、確かめる組み合わせが2倍になります。どちらか片方だけ確かめて終わりにしないことが大切です。

影は見えにくくなる

暗い背景の上では、カードの影がほとんど見えません。明るい表示で影だけでカードの範囲を見せていると、暗い表示では境目がなくなります。1のカードは、影ではなく枠線(--kd-dm-line)で区切っているので、どちらの表示でも形が分かります。

ロゴや画像も、暗い表示用を用意する

紺色の文字で作ったロゴは、暗い背景の上では見えなくなります。画像は <picture> と media 属性を使うと、暗い表示のときだけ別の画像に差しかえられます。

HTML
<picture>
  <source srcset="logo-white.svg" media="(prefers-color-scheme: dark)">
  <img src="logo.svg" alt="KAKERU">
</picture>

ただし、この書き方は端末の設定にだけ合わせるので、4のボタンで切りかえた表示には合いません。ボタンを付けるなら、ロゴはSVGにして、色を currentColor(その場所の文字の色)にしておくと、文字といっしょに色が変わります。

切りかえボタンは、読み上げにも状態を伝える

4のボタンは本物の <button> なので、キーボードのTabキーで選んで、EnterキーやSpaceキーで押せます。aria-pressed を付けているので、読み上げソフトでは、ボタンの名前といっしょに、押されているかどうか(今どちらの表示か)も伝わります。ボタンの文字は「ダークモード」のまま変えません。押すたびに文字まで変えると、今どちらなのかが分かりにくくなるためです。丸が動く速さは0.2秒で、動きを減らす設定の人には動かしません。

一瞬、明るい画面が出ることがある

4のJavaScriptを </body> の直前に置くと、ページが表示されたあとに、保存しておいた表示が反映されます。端末は明るい表示で、ボタンで暗い表示を選んでいた人には、開いた瞬間だけ白い画面が見えることがあります。気になる場合は、localStorage を読んで data-theme を付ける部分だけを、<head> の中の短いスクリプトに移します。

やりすぎ注意

ダークモードに対応するなら、全部のページで、全部の部品を確かめる必要があります。トップページだけ暗くなって、お問い合わせフォームや古いブログ記事が白いまま、という状態は、対応していないときより目立ちます。対応する範囲を決めて、最後まで確かめられるときに入れます。

トミー

ダークモードは「付けるか、付けないか」より、「最後まで面倒を見られるか」で決めています。
半分だけ暗いサイトは、白いままのサイトより、作りの粗さが目に入ります。

AIに頼むなら

ダークモードは、Claudeなどの生成AIに頼むとすぐ作ってもらえます。そのときは「今の色の管理のしかた」「暗い表示の色」「部品の範囲」「ボタンの有無」まで伝えると、手直しが少なく済みます。

このCSSの色を :root の変数にまとめて、prefers-color-scheme: dark のときだけ入れかえて。
暗い表示の背景は #0F1B26、文字は #E6EEF4、リンクは #7DD3FC。color-scheme: light dark も付けて。
文字と背景の明るさの差が、ライトとダークの両方で4.5対1以上あるか、一覧にして。

AIが出したコードは、そのまま使う前に、次の3つを確かめます。

  • filter: invert() で全体を反転していないか。ページ全体の色を反転させる書き方は短いですが、写真やロゴまで反転して、色がおかしくなります。色は変数で入れかえる形に直します
  • 色が直接書かれた場所が残っていないか。変数にまとめたつもりでも、ボタンやフォームなどに #FFFFFF のような色が直接残っていると、そこだけ白く光ります。暗い表示にして、全部のページを開いて確かめます
  • リンクやブランドの色が、明るい表示のままになっていないか。濃い色は暗い背景で沈みます。暗い表示用の色が別に決まっているかを見ます

KAKERUならこう使う/使わない

最後に、作り手としての判断です。

使う場面

  • 色を変数で管理している、新しく作るサイト。最初から色の表を作っておけば、ダークモード用の表を足すだけで済みます。作り始める段階で、対応するかどうかを決めます
  • 夜に長く読まれるページ。読み物やマニュアル、ツールのように、暗い部屋で長く見られるページは、暗い表示に合わせる意味が大きいです
  • フォームのあるページの color-scheme。サイト全体を暗くしない場合でも、暗い表示に対応したページなら、2の1行で入力欄の白浮きを防げます

使わない場面

  • 白い背景が前提の写真やロゴが多いサイト。写真の白いふちや、透過していないロゴが、暗い背景の上で四角く浮きます。全部の画像を作り直せないなら、明るい表示のまま整えます
  • 色がCSSのあちこちに直接書かれている、既存のサイト。一部だけ変数にしても、残った場所が白く光ります。まず色を整理するほうが先です
  • 確かめる時間がとれないとき。ページごとに、ライトとダークの両方で読みやすさを確かめる必要があります。その時間がとれないときは、入れないほうがサイトの印象を守れます
トミー

ダークモードに対応するかどうかは、サイトを作り始めるときに決めます。
あとから足すと、色の整理からやり直すことになるので、最初の色の表にダーク用の列を作っておくのがいちばん楽です。

まとめ

  • @media (prefers-color-scheme: dark) で、見る人が暗い表示を選んでいるかを読み取れる
  • 色を :root の変数にまとめておくと、入れかえは色の表の1か所で済む
  • color-scheme: light dark で、入力欄やスクロールバーなど、ブラウザが描く部分も暗くなる
  • light-dark() は1行で2色を書き分けられる。比較的新しいので、先に普通の色を書いておく
  • 暗い表示では、真っ黒・真っ白を避け、リンクやブランドの色は1段明るくする
  • ボタンで切りかえるなら aria-pressed で状態を伝え、選んだ表示は localStorage に残す

まずは1のコードをコピーして、自分のパソコンやスマホをダークモードにして開いてみてください。そのあと、変数の色を1つずつ変えると、暗い表示での色の選び方がよく分かります。

参考にした資料

いずれも2026年10月29日に確認しました。ブラウザの対応状況は変わることがあります。最新の情報は各ページでご確認ください。

ふだんの制作の話や、作ったものの裏側は Instagram と X でも出しています。

関連記事

チェックボックスの見た目をCSSで変える。見本とコード
影の付け方をCSSで覚える。box-shadowの見本とコード
入力欄を選んだら色が変わる。見本とコード
すりガラスの背景をCSSで作る。backdrop-filterの見本とコード
ダークモードにCSSで対応する。ライトとダークのカードの見本とコード

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

CAPTCHA