# ダークモードでコントラストが足りなくなる文字とアイコン

Source: https://easeweb.dev/learn/dark-theme-contrast
Topics: 色とコントラスト > ダークモードでも文字やアイコンが読める (WCAG 1.4.3, 1.4.11)
The fix: 控えめな文字やアイコンなど、色ごとに役割のトークンを用意してダークテーマで定義し直すか、light-dark()で各色をダーク用の値と組にして書きます。どちらの場合も、SVGアイコンはcurrentColorで描き、ボタンの色を受け取るようにします。
Test with: dark mode, contrast measurement, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme https://hidde.blog/making-single-color-svg-icons-work-in-dark-mode/

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

レシピサイトに、端末の設定に合わせて切り替わるダークテーマがあります。デザインファイルでは、大きな面の色を入れ替えてダーク版を作りました。ページとカードは薄いグレーと白から深いスレート色に、タイトルは黒に近い色から白に近い色に変わります。タイトルの下で所要時間と人数を示すグレーの行と、その横の保存・共有のアイコンは、ライト版の色をそのまま使いました。ライトテーマでは、デザインの狙いどおり控えめで、しかもはっきり見えます。開発者はそのとおりに実装し、ページ、カード、タイトルの色をトークンにして、`prefers-color-scheme: dark` のブロックで定義し直しました。

端末をダークモードにすると、サイトも切り替わります。タイトルは問題なく読めます。ところが、その下の「25分 · 4人分」は暗いカードの上の中間のグレーになり、保存と共有のアイコンは暗いグレーの上の暗いグレーになります。場所を知っている人には見えても、知らない人には見えません。

## 確認できる問題

ダークテーマが定義し直すのは、トークンになっている色だけです。グレーは `.meta` に16進数の値で直接書かれ、アイコンの色は各SVGの `fill` 属性に書かれているため、テーマが変わっても変わりません。

- 所要時間の行の `#6b7280` は、白いカードの上で4.83:1、`#1f2937` のカードの上で3.04:1です。この大きさの文字に、WCAG 1.4.3は4.5:1を求めています。
- アイコンの `#374151` は、白の上で10.31:1、暗いカードの上で1.42:1です。各ボタンが何かを示すのはアイコンだけなので、1.4.11は周りの色に対して3:1を求めています。

どちらの基準も、ページが実際に表示する色で測ります。ダークモードの利用者には、このページはダークテーマで表示されます。切り替えているのはブラウザーではなく、サイト自身のCSSです。画面上の色はどれも、サイトが指定した色か、ダークテーマ用に指定しなかった色です。

## 利用者への影響

システムの設定に合わせるサイトは、ダークモードの利用者全員にダークテーマを表示します。夜だけ自動でダークモードになるよう設定したスマートフォンでも同じです。読者の多くは、このサイトのテーマを自分で選んだわけではありません。

## 自動チェックとAIの限界

axeを含む自動チェックは、ブラウザーが今使っている配色でページを測ります。ヘッドレスブラウザーは、指定しない限りライトモードです。ライトモードではこのページのどの色も基準を満たすため、チェックの結果は問題なしになります。EaseWebの診断は、ダークモードで色が変わるページについてはダークモードをエミュレートして測り直しますが、対象はシステムの設定に合わせるテーマだけです。サイト独自の切り替えボタンでしか有効にならないテーマには届きません。ダークテーマのCSSを読んでも、修正前のコードの問題は見つかりません。ダークのブロックには、用意されたトークンについては漏れがなく、足りないのはコンポーネントの側だからです。AIが生成するダークテーマも同じところで止まることがあり、背景と本文の色は切り替えても、カードの中のグレーやアイコンの塗りはそのまま残ります。

## 修正前：ダークテーマが背景と文字の色だけを切り替える

```html
<article class="recipe">
  <h2>ポロねぎのグリル、ロメスコソース添え</h2>
  <p class="meta">25分 · 4人分</p>
  <button type="button" class="icon-button" aria-label="保存">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#374151" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="共有">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#374151" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  color: #6b7280; /* 白の上で4.83:1 */
}
```

ライトテーマでは基準を満たします。ダークテーマでは次のようになります。

- 所要時間の行は `#6b7280` のままで、暗いカードの上で3.04:1です。
- 保存と共有のアイコンは `fill="#374151"` のままで、暗いカードの上で1.42:1です。

## 修正後：すべての色にテーマのトークンを使う

```html
<article class="recipe">
  <h2>ポロねぎのグリル、ロメスコソース添え</h2>
  <p class="meta">25分 · 4人分</p>
  <button type="button" class="icon-button" aria-label="保存">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="共有">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
  --muted: #6b7280; /* 白の上で4.83:1 */
  --icon: #374151;  /* 白の上で10.31:1 */
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
    --muted: #9ca3af; /* #1f2937の上で5.78:1 */
    --icon: #d1d5db;  /* #1f2937の上で9.96:1 */
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  color: var(--muted);
}
.icon-button {
  color: var(--icon);
}
```

1. コンポーネントが使う役割ごとにトークンを用意します。ここでは控えめな文字の `--muted` とアイコンの `--icon` で、値はライトテーマのものです。ライトテーマの見た目は変わりません。
2. 2つのトークンをダークのブロックで定義し直し、暗いカードの上で測ります。グレーは `#9ca3af`（5.78:1）、アイコンは `#d1d5db`（9.96:1）に明るくなります。
3. `.meta` の色を、16進数の値から `var(--muted)` に変えます。
4. SVGのパスを `fill="currentColor"` にし、`.icon-button` の色を `--icon` から取ります。アイコンはどちらのテーマでも、ボタンの色で描かれます。

これで修正前の例は直り、どちらのテーマでも1.4.3と1.4.11を満たします。ダーク版でも、控えめなグレーの行と柔らかいアイコンというデザインはそのままで、一段明るくなるだけです。

## 修正後：light-dark関数で各色をダーク用の値と組にする

```html
<article class="recipe">
  <h2>ポロねぎのグリル、ロメスコソース添え</h2>
  <p class="meta">25分 · 4人分</p>
  <button type="button" class="icon-button" aria-label="保存">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="共有">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  color-scheme: light dark;
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  /* 白の上で4.83:1、#1f2937の上で5.78:1 */
  color: light-dark(#6b7280, #9ca3af);
}
.icon-button {
  /* 白の上で10.31:1、#1f2937の上で9.96:1 */
  color: light-dark(#374151, #d1d5db);
}
```

1. ルートに `color-scheme: light dark` を加えます。ページが両方の配色に対応していることをブラウザーに伝える指定で、これにより `light-dark()` が端末の設定に従います。この指定がないと、`light-dark()` は常に1つ目の値を使います。
2. グレーを `light-dark(#6b7280, #9ca3af)` と書きます。1つ目がライトテーマの値、2つ目がダークテーマの値で、暗いカードの上で5.78:1です。
3. SVGのパスを `fill="currentColor"` にし、`.icon-button` に `light-dark(#374151, #d1d5db)` の組を指定します。暗いカードの上で9.96:1です。

これで修正前の例は直り、どちらのテーマでも1.4.3と1.4.11を満たします。`light-dark()` は2024年5月から、主要なブラウザーの最新版すべてで使えます。古いブラウザーは宣言全体を無視するため、対応が必要なら各組の前に `color: var(--ink);` を書いておきます。古いブラウザーでは所要時間とアイコンが本文の色で表示され、デザインより目立ちますが読めます。`color-scheme` は、スクロールバーやスタイルを指定していないフォームコントロールなど、ブラウザー自身が描く部分もダークの見た目にします。

## どの方法を選ぶか

どちらも基準を満たし、どちらも各色にダーク用の値を持たせます。違うのは、その値を置く場所です。テーマのトークンはすべての色を1か所にまとめるため、デザインシステムの配色とコントラスト比をまとめて確認でき、トークンを使う新しいコンポーネントは、誰も意識しなくても両方のテーマで正しくなります。複数のコンポーネントが同じグレーを共有するなら、トークンを選びます。`light-dark()` は各組をコンポーネントの1行に置くため、色にダーク用の値があることがレビューですぐ分かり、2つ目のブロックも要りません。独自の色を持つコンポーネントや、色の数が少ないサイトに向いています。どちらの場合も、アイコンは `currentColor` にする必要があります。SVGに直接書かれた色には、トークンも組も届かないからです。

## 実装の判断

各色は、ページの背景だけでなく、ダークテーマで置かれるすべての背景の上で測ります。ダークテーマでは、明るい面で奥行きを表すことが多いため、`#111827` のページの上で基準を満たすグレーも、より明るいカード、バナー、メニューの上ではコントラストが下がり、届かないことがあります。

システムの設定ではなく、サイト独自の切り替えボタンで変わるテーマにも、同じ値が必要です。ボタンでルートに `data-theme="dark"` などの属性を設定し、メディアクエリーと同じトークンをそのセレクターでも定義し直すか、ボタンでルートの `color-scheme` を切り替えて `light-dark()` を従わせます。WCAGの1.4.3の解説ページは、利用者が選んだ色が基準を下回ることを「必要な値に戻す仕組みがあれば」認めています。ライトテーマにワンクリックで戻せる切り替え式のダークテーマがこれに当たるかどうかは、WCAGでもまだ結論が出ていません。システムの設定に合わせるテーマには、そのワンクリックがありません。ダークモードの利用者全員が受け取るページそのものです。いずれにしても、読むためにダークモードを必要としている人もいるため、提供するテーマはすべて測ってください。

Tailwindでは、色のユーティリティごとに `dark:` の組が必要です（`text-gray-500 dark:text-gray-400`）。1つでも書き忘れると、ライトの値のまま残ります。色をダークテーマで変わるテーマ変数として定義すれば、ユーティリティが上のトークンと同じく役割を表すようになり、書き忘れる組がなくなります。

`<img>` のファイルとして表示するアイコンは、`currentColor` を使えません。ページのCSSがファイルの中まで届かないためです。単色のアイコンはSVGとしてインラインで書くか、ダークテーマ用に別のファイルを用意します。SVGファイルの中に `prefers-color-scheme` のクエリーを書く方法もありますが、Safariは `<img>` で読み込んだSVGでは、このクエリーを長く無視してきました。ロゴの文字は1.4.3の対象外ですが、黒いロゴが暗いページに置かれると、表示が壊れたように見えます。`<picture>` に `<source media="(prefers-color-scheme: dark)">` を加え、明るい版を指定します。

フォーカスの枠も同じ問題を起こします。`#1a4fd0` の枠は白の上で6.83:1ですが、`#1f2937` のカードの上では2.15:1です。枠にもトークンか組を用意します。iframeで埋め込んだウィジェットは、ウィジェット側の配色のまま表示されます。ダークの設定があれば使い、なければ明るいまま残します。ウィジェット自体の色が基準を満たしていれば問題ありません。

## 修正の確認

1. 端末、またはブラウザーの `prefers-color-scheme` のエミュレーションをダークにして再読み込みし、すべての文字の色を、すぐ後ろの背景に対して測る。4.5:1以上、大きな文字は3:1以上あるか確認する。
2. ダークモードで、アイコンだけのコントロールと入力欄の枠を、周りの色に対して測る。3:1以上あるか確認する。
3. コンポーネントのCSSとSVGから、テーマのトークンでも `light-dark()` の組でもない色（`#`、`rgb(`、`fill="`、`stroke="`）を探し、それぞれにダーク用の値を持たせる。
4. ダークモードで、ロゴ、イラスト、グラフ、埋め込みのウィジェットをすべて確認し、暗いページの上で暗い形になっているものがないか確認する。
5. ライトモードに戻し、ライトテーマのコントラスト比がどれも変わっていないか確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。テーマのトークンや共通のコンポーネントのスタイルを変更した際は、再度確認します。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。色の値とトークン名は例のため、実際の配色を実際の背景で測定してください。ライトテーマでのコントラスト比は、[ブランドカラーや写真の上でコントラストが足りない文字](/ja/learn/text-contrast)と[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)で扱います。Windowsのコントラスト テーマは、サイトの色を利用者が選んだ色に置き換えるもので、別の問題です。[ハイコントラストモードで消えるフォーカス・アイコン・選択状態](/ja/learn/forced-colors)で扱います。必要とする人と一緒に、両方のテーマでページ全体を確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
