CSSで、コントラストを上げる設定の人に合わせて色を変える方法
OS などでコントラストを上げる設定にしている人には、CSS の prefers-contrast メディア特性で、文字や枠線の色を濃くしたページを見せられます。色をカスタムプロパティにまとめておけば、@media (prefers-contrast: more) の中では値を上書きするだけで済みます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153(ヘッドレス)です。設定の切り替えは、Playwright の contrast オプションと、Chrome DevTools Protocol の Emulation.setEmulatedMedia によるエミュレーションで行いました。OS のコントラストの設定を実際に変えたときの動き、OS の強制カラーモード(forced-colors: active)を有効にしたときの prefers-contrast の値、Firefox と Safari では試していません。ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。
prefers-contrast の4つの値
Media Queries Level 5 の仕様では、値は次の4つです。
| 値 | 意味 |
|---|---|
no-preference |
利用者がコントラストについて何も設定していない |
more |
コントラストを上げたい |
less |
コントラストを下げたい |
custom |
決まった色の組み合わせを使いたいが、その色のコントラストは more にも less にも当たらない |
custom は、強制カラーモード(forced-colors: active)で、コントラストが特に高くも低くもない配色を選んだ人に当てはまる値です。
検証環境でエミュレーションの値を切り替え、matchMedia() で当てはまるかを調べた結果が次の表です。
| エミュレーションした値 | no-preference |
more |
less |
custom |
値なしの (prefers-contrast) |
|---|---|---|---|---|---|
| なし | true | false | false | false | false |
more |
false | true | false | false | true |
less |
false | false | true | false | true |
custom |
false | false | false | true | true |
値を書かない @media (prefers-contrast) は、more・less・custom のどれでも当てはまります。仕様のメモには、値を書かない形で高コントラスト用のスタイルを当てるのは誤りだと書かれています。コントラストを下げたい人にまで、高コントラストの色を見せることになるためです。色を濃くするときは more と書きます。
色をカスタムプロパティにまとめて、more のときだけ上書きする
お知らせのカードを例にします。色はすべて :root のカスタムプロパティにして、.card ではそれを使うだけにします。
:root {
--text: #1f2a30;
--muted: #66727a;
--bg: #ffffff;
--border: #d5dde1;
--accent: #067a98;
--on-accent: #ffffff;
}
.card {
color: var(--text);
background: var(--bg) linear-gradient(#eefafd, #ffffff);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
.card .meta { color: var(--muted); }
.card button {
color: var(--on-accent);
background: var(--accent);
border: 1px solid var(--accent);
}
@media (prefers-contrast: more) {
:root {
--text: #000000;
--muted: #3d474d;
--border: #1f2a30;
--accent: #045d75;
}
}
@media (prefers-contrast) {
.card {
box-shadow: none;
background-image: none;
}
}
最後の @media (prefers-contrast) は、影とグラデーションを消すためのものです。仕様のメモでは、背景画像を単色にする、飾りのグラデーションをやめる、枠線の画像や影を単色の枠線に替える、といった見た目を簡単にする変更は、more と less のどちらの人にも合うので、値を書かない形で書いてよいとされています。

検証環境で、それぞれの色の組み合わせのコントラスト比を、WCAG 2.2 のコントラスト比の式で計算しました。
| 組み合わせ | 設定なし | more |
|---|---|---|
| 日付の文字と白い背景 | 4.94 | 9.52 |
| ボタンの文字(白)と背景 | 4.95 | 7.43 |
| 枠線と白い背景 | 1.38 | 14.66 |
設定なしのときも、文字は WCAG 2.2 の達成基準 1.4.3(レベル AA)の 4.5:1 を満たす色にしています。カードの背景は薄い水色(#eefafd)から白へのグラデーションで、いちばん濃い #eefafd に対しても、日付の文字のコントラスト比は 4.64 でした。prefers-contrast は設定した人にしか効かないので、ふだんの色は、それだけで読める色にしておく必要があります。more のときは、達成基準 1.4.6(レベル AAA)の 7:1 を超える色にしました。
less をエミュレーションしたときは、色は設定なしと同じで、影とグラデーションだけが消えました。less 用に色を薄くするなら、@media (prefers-contrast: less) を別に書きます。
メディアクエリは、元のスタイルより後に書く
@media の中に書いても、セレクターの詳細度は変わりません。詳細度が同じ宣言どうしでは、CSS の中で後に書いたほうが使われます。
上の CSS の2つの @media を .card のルールより前に移して、more をエミュレーションすると、色は濃くなりましたが、影とグラデーションは残りました。
--- prefers-contrast: more
本文の文字 = rgb(0, 0, 0)
メタ情報の文字 = rgb(61, 71, 77)
枠線 = rgb(31, 42, 48)
ボタンの背景 = rgb(4, 93, 117)
box-shadow = rgba(0, 0, 0, 0.12) 0px 4px 12px 0px
background-image = linear-gradient(rgb(238, 250, 253), rgb(255, 255, 255))
色が変わったのは、@media の中で上書きしているのが .card のプロパティではなく、:root のカスタムプロパティだからです。この :root のルールは、元の :root のルールより後にあるので、.card との順番に関係なく使われます。影やグラデーションのように .card のプロパティを直接上書きするルールは、.card より後に置きます。
なお、仕様の古い版にあった high と low は、仕様の変更履歴にあるとおり、それぞれ more と less に名前が変わり、今の仕様にはありません。検証環境で more をエミュレーションしても、@media (prefers-contrast: high) の中のスタイルは使われませんでした。
JavaScript で設定を読む
JavaScript からは matchMedia() で読めます。
const mq = matchMedia('(prefers-contrast: more)');
console.log(mq.matches);
mq.addEventListener('change', (e) => {
console.log(e.matches);
});
検証環境では、ページを開いたままエミュレーションを more にすると、matches が true の change イベントが届き、no-preference に戻すと false のイベントが届きました。
強制カラーモードとの違い
forced-colors は、ブラウザーが利用者の選んだ配色を、ページに強制的に当てているかどうかを表します。MDN によると、強制カラーモードでは color や background-color、border-color などはブラウザーが決めた色になり、box-shadow は none になります。検証環境でも forced-colors: active をエミュレーションすると、日付の文字の色は rgb(0, 0, 0)、カードの box-shadow は none になりました。
prefers-contrast は、利用者の希望を伝えるだけで、色はページの CSS が決めます。強制カラーモードのようにブラウザーが色を替えることはないので、more のときの色は、上のように自分で用意します。
仕様では、強制カラーモードのときは、選んだ配色に応じて prefers-contrast の more・less・custom のどれかも当てはまるとされています。Chromium のエミュレーションでは forced-colors と prefers-contrast は別々に切り替わり、forced-colors: active だけをエミュレーションしたときの prefers-contrast は no-preference のままでした。
テストで more の表示を確かめる
Playwright では、ブラウザーコンテキストの contrast オプションで more を指定できます。
const context = await browser.newContext({ contrast: 'more' });
const page = await context.newPage();
// ページを開いたあとに切り替えるときは
await page.emulateMedia({ contrast: 'no-preference' });
検証環境では、contrast: 'more' で開いたページで、日付の文字の色が rgb(61, 71, 77)(--muted を上書きした #3d474d)になり、emulateMedia() で no-preference に戻すと rgb(102, 114, 122) に戻りました。Playwright 1.63.0 の型定義では、contrast に指定できるのは 'no-preference' と 'more' だけです。less と custom は、この記事では Chrome DevTools Protocol の Emulation.setEmulatedMedia で切り替えました。
const cdp = await page.context().newCDPSession(page);
await cdp.send('Emulation.setEmulatedMedia', {
features: [{ name: 'prefers-contrast', value: 'less' }],
});
ブラウザーの対応
互換性データでの対応状況です。
| ブラウザー | prefers-contrast |
|---|---|
| Chrome・Edge(Android の Chrome を含む) | 96 から |
| Firefox(Android を含む) | 101 から |
| Safari | 14.1 から |
| iOS の Safari | 14.5 から |
Chrome・Firefox・Safari のどれでも使えるのは、Chrome 96、Firefox 101、Safari 14.1 からです。





