はじめに
背景色が変わるたびにテキスト色を手動で調整するのは手間がかかり、色の組み合わせが増えるほど見落としも生じやすくなります。WCAGのコントラスト比計算式をJavaScriptで実装すれば、任意の背景色に対して黒か白のどちらを使うべきかを自動判定できます。
本記事は、コントラスト判定関数の実装から、カラーピッカーやCSSアニメーションと組み合わせたUIへの応用まで紹介します。
コントラスト比の計算式
テキストの読みやすさを判定するには、WCAG(Web Content Accessibility Guidelines)が定めたコントラスト比の計算式を使います。必要な手順は次の3ステップです。
まずRGB各チャンネルを0〜1の範囲に変換し、ガンマ補正を除去してリニア値にします。次に、そのリニア値を R:G:B = 0.2126:0.7152:0.0722 の重みで合算して相対輝度(L)を求めます。最後に、白(L=1)と黒(L=0)それぞれとのコントラスト比を計算して高い方のテキスト色を採用します。
sRGB = channel / 255
linear = sRGB <= 0.04045 ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ^ 2.4
L = 0.2126 * R + 0.7152 * G + 0.0722 * B
白とのコントラスト比 = 1.05 / (L + 0.05)
黒とのコントラスト比 = (L + 0.05) / 0.05この計算式は、目の色感度がRGBで均等ではないことを考慮した人間の知覚ベースのモデルです。緑チャンネルの重みが最も大きいのはそのためです。
基本の実装
計算式をJavaScriptにまとめます。16進数のカラーコードを受け取り、'#ffffff'か'#000000'を返す関数です。
See the Pen bg-color-text-contrast-js v1 by watanabe (@web-sourcecode) on CodePen.
HTML
<div class="swatch" data-bg="#3498db"><span>#3498db(青)</span></div>
<div class="swatch" data-bg="#e67e22"><span>#e67e22(オレンジ)</span></div>
<div class="swatch" data-bg="#1a237e"><span>#1a237e(深い青)</span></div>
<div class="swatch" data-bg="#f1c40f"><span>#f1c40f(黄)</span></div>
<div class="swatch" data-bg="#2ecc71"><span>#2ecc71(緑)</span></div>CSS
body {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 24px;
font-family: sans-serif;
background: #f5f5f5;
}
.swatch {
padding: 20px 28px;
border-radius: 8px;
font-weight: bold;
font-size: 0.95rem;
}JavaScript
/**
* 16進数カラーコードを受け取り、
* 最もコントラスト比が高いテキスト色(黒か白)を返す
* @param {string} hex - '#rrggbb' 形式
* @returns {'#ffffff' | '#000000'}
*/
function getContrastColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
// ガンマ補正を除去してリニア値に変換
const toLinear = (c) => {
const s = c / 255;
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
// 相対輝度
const L = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
const contrastWithWhite = 1.05 / (L + 0.05);
const contrastWithBlack = (L + 0.05) / 0.05;
return contrastWithWhite > contrastWithBlack ? '#ffffff' : '#000000';
}
// 各スウォッチに背景色とテキスト色を適用
document.querySelectorAll('.swatch').forEach((el) => {
const bg = el.dataset.bg;
el.style.backgroundColor = bg;
el.style.color = getContrastColor(bg);
});data-bg 属性に色を渡してJavaScriptで一括適用しています。色の種類が増えても関数を1か所呼ぶだけで対応できます。
カラーピッカーと組み合わせたUI
ユーザーが背景色を自由に選べるUIです。選んだ色のコントラスト比とWCAGの達成レベルもリアルタイムで表示します。
See the Pen bg-color-text-contrast-js v2 by watanabe (@web-sourcecode) on CodePen.
HTML
<label class="picker-label">
背景色を選択:
<input type="color" id="colorPicker" value="#3498db">
</label>
<div class="preview" id="preview">
<p class="preview__title">サンプルテキスト</p>
<p class="preview__info" id="previewInfo"></p>
</div>CSS
body {
display: flex;
flex-direction: column;
gap: 20px;
padding: 32px;
font-family: sans-serif;
background: #f5f5f5;
}
.picker-label {
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 10px;
}
.preview {
padding: 36px 40px;
border-radius: 12px;
transition: background-color 0.15s;
}
.preview__title {
margin: 0 0 8px;
font-size: 1.3rem;
font-weight: bold;
transition: color 0.15s;
}
.preview__info {
margin: 0;
font-size: 0.875rem;
opacity: 0.85;
transition: color 0.15s;
}JavaScript
function getContrastColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const toLinear = (c) => {
const s = c / 255;
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
const L = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
const contrastWithWhite = 1.05 / (L + 0.05);
const contrastWithBlack = (L + 0.05) / 0.05;
const maxContrast = Math.max(contrastWithWhite, contrastWithBlack);
return {
color: contrastWithWhite > contrastWithBlack ? '#ffffff' : '#000000',
ratio: maxContrast.toFixed(2),
level: maxContrast >= 7 ? 'AAA' : maxContrast >= 4.5 ? 'AA' : '基準未満',
};
}
const picker = document.getElementById('colorPicker');
const preview = document.getElementById('preview');
const infoEl = document.getElementById('previewInfo');
const titleEl = preview.querySelector('.preview__title');
function update(hex) {
const { color, ratio, level } = getContrastColor(hex);
preview.style.backgroundColor = hex;
titleEl.style.color = color;
infoEl.style.color = color;
infoEl.textContent = `コントラスト比 ${ratio}:1 WCAG ${level}`;
}
picker.addEventListener('input', (e) => update(e.target.value));
update(picker.value);コントラスト比の数値とWCAGレベルをリアルタイムで確認できるため、デザイン確認ツールとしてそのまま使えます。
アニメーションとの組み合わせ
CSSアニメーションで背景色が変化する中で、requestAnimationFrameとgetComputedStyleを使ってテキスト色を毎フレーム更新します。getComputedStyleはアニメーション途中の計算済み色も返すため、この組み合わせが成立します。
背景のカラーアニメーションには@propertyを使います。syntax: '<color>'で型を登録することで、@keyframes内のカスタムプロパティが色として補間されるようになります(Chrome 85+、Safari 16.4+、Firefox 128+)。
See the Pen bg-color-text-contrast-js v3 by watanabe (@web-sourcecode) on CodePen.
HTML
<div class="banner" id="banner">
<h2 class="banner__title">アニメーション中もコントラストを維持</h2>
<p class="banner__ratio" id="bannerRatio">計算中...</p>
</div>CSS
@property --banner-bg {
syntax: '<color>';
inherits: false;
initial-value: hsl(220, 80%, 45%);
}
body {
display: grid;
place-items: center;
min-height: 100vh;
margin: 0;
font-family: sans-serif;
background: #ddd;
}
.banner {
--banner-bg: hsl(220, 80%, 45%);
background-color: var(--banner-bg);
padding: 48px 72px;
border-radius: 16px;
text-align: center;
animation: bg-shift 5s ease-in-out infinite alternate;
}
.banner__title {
margin: 0 0 10px;
font-size: 1.4rem;
}
.banner__ratio {
margin: 0;
font-size: 0.875rem;
opacity: 0.85;
}
@keyframes bg-shift {
to { --banner-bg: hsl(30, 90%, 60%); }
}JavaScript
// getComputedStyle は "rgb(r, g, b)" 形式を返すためRGB直接受け取りに対応
function getContrastColorFromRgb(r, g, b) {
const toLinear = (c) => {
const s = c / 255;
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
const L = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
const contrastWithWhite = 1.05 / (L + 0.05);
const contrastWithBlack = (L + 0.05) / 0.05;
const maxContrast = Math.max(contrastWithWhite, contrastWithBlack);
return {
color: contrastWithWhite > contrastWithBlack ? '#ffffff' : '#000000',
ratio: maxContrast.toFixed(2),
};
}
const banner = document.getElementById('banner');
const ratioEl = document.getElementById('bannerRatio');
function syncTextColor() {
// アニメーション中の現在の背景色を取得("rgb(r, g, b)" 形式)
const bgColor = getComputedStyle(banner).backgroundColor;
const [r, g, b] = bgColor.match(/\d+/g).map(Number);
const { color, ratio } = getContrastColorFromRgb(r, g, b);
banner.style.color = color;
ratioEl.textContent = `コントラスト比 ${ratio}:1`;
requestAnimationFrame(syncTextColor);
}
syncTextColor();syncTextColorは毎フレーム呼ばれます。テキスト量や要素数が少なければパフォーマンスは問題になりませんが、大量の要素に適用する場合はスロットリングを検討してください。
CSS color-contrast()について
CSS Color Level 5仕様には、このJavaScript実装と同等のことをCSSだけで行うcolor-contrast()関数が定義されています。
/* 将来的にはこれだけで同じことができる */
.card {
background-color: var(--accent);
color: color-contrast(var(--accent) vs #000, #fff);
}ただし執筆時点(2026年)でのブラウザ対応状況は流動的で、本番環境では使えないケースがあります。caniuse.comで最新の対応状況を確認してから採用の判断をしてください。
まとめ
WCAGのコントラスト比計算式をJavaScriptで実装することで、任意の背景色に対して最適なテキスト色を今すぐ自動選択できます。計算の核はgetContrastColor()関数1つで、カラーピッカーやアニメーションなど用途に応じて組み合わせ方を変えるだけです。
color-contrast()がブラウザで広く使えるようになれば、同じロジックをCSSネイティブで書ける日が来ます。
ポイント
- WCAGコントラスト比は相対輝度から計算する。ガンマ補正の除去が必要
- 白とのコントラスト比
1.05 / (L + 0.05)と黒との比(L + 0.05) / 0.05を比較して高い方を採用する getComputedStyleはアニメーション中の計算済み色を返すため、requestAnimationFrameと組み合わせると動的な背景色にも追随できる- CSSの
color-contrast()は同等の機能を持つが、ブラウザ対応は要確認