北川です。
先日、任意の色の背景がある時、テキストの文字色を黒か白どちらを使うのが適当か?という問題に当たりました。最終的にまあまあ満足のいく結果が得られたので記事にしました。
背景色と文字色の問題
これは色々な背景色について黒か白の文字色の選択を筆者の主観に基づき決めた画像です。社内でフィードバックを求めたところ「黄緑や水色が見にくい」と不評でした。
このように当人としては見やすいと思っても、他の人から見やすいとは限りません。
またよくある問題として、一律黒文字テキストでスタイリングしていたがボタンの塗りつぶしが濃い色のときに見辛くなってしまった、ということもあると思います。
文字色が黒か白かに限らずそれ以外の任意の文字色でも同様な一般的な問題かと思います。
毎回どうすれば良いか判断するのも手間がかかって仕方がありません。何か良い解決方法はないものでしょうか?
WCAG
さて、WCAGというものがあります。WCAG(Web Content Accessibility Guidelines)は、ウェブコンテンツのアクセシビリティを向上させるための国際的な標準です。
今回の問題も文字の見やすさといったアクセシビリティに関連した話なので、ここにヒントがありそうです。
文字色決定戦略を決める
Web Content Accessibility Guidelines (WCAG) 2.2
The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for the following
Contrast (Minimum) の項に関連しそうなことが書いてありました。contrast ratio というものが少なくとも 4.5:1 であるべきだと。なるほどまったく分かりません。
Contrast (Enhanced) の項には、contrast ratio は少なくとも 7:1 であるべきともあります。どうも contrast ratio は高ければ高いほど良さそうです。
次に、contrast ratio とは何かが気になります。
(L1 + 0.05) / (L2 + 0.05), where
L1 is the relative luminance of the lighter of the colors, and
L2 is the relative luminance of the darker of the colors.
と、contrast ratio の項にありました。どうやら contrast ratio は relative luminance という変数に依存した式で表せるようです。
さらに掻い摘んだ説明をChatGPTに聞いてみると「コントラスト比は、テキストや図形などの要素とその背景の間の明るさの差を示します。これは、テキストを読みやすくするために必要な要素である」と教えてくれました。コントラストと文字色との話がつながりましたね。この比が 4.5:1 または 7:1 より高ければクッキリハッキリ見えやすいということです。
次に、relative luminance とは何かが気になります。これが分かればついに文字色決定戦略を決められる予感がします!
the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white
For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as:
略
と、relative luminance の項にありました。さっきの式よりも複雑ですが、知らない変数がありません!RGB値に依存した値が relative luminance のようです。
さらに掻い摘んだ説明をChatGPTに聞いてみると「相対輝度(Relative Luminance)は、色の明るさを表す指標の一つです。色の相対輝度は、その色がどれだけ明るいかを示し、通常、0から1までの範囲で表されます。0は完全な黒を示し、1は完全な白を示します。」と教えてくれました。要するに色の明るさのことでした。
「ある背景色について、文字色1と文字色2のどちらがよりアクセシビリティの観点で見やすいか?」
これまでの要素を踏まえると以下のように言い換えられそうです。
「色1について、色2と色3のどちらがよりコントラスト比(contrast ratio)が高いか?」
ここまでくると、このお題を解決する戦略が決められそうです。その戦略はこうです。
- step1: 色1,2,3 についてそれぞれのRGB値を計算する
- step2: 色1,2,3 についてそれぞれの相対輝度(relative Luminance)を step1 で求めたRGB値を使って計算する
- step3: 色1,2のコントラスト比と、色1,3のコントラスト比(contrast ratio)を step2 で求めた相対輝度を使って計算する
- step4: よりコントラスト比が大きい方がアクセシビリティの観点で見やすいので採用する
実装
// 色1について、色2と色3のどちらとのコントラスト比が高いかを返す const selectHigherContrastRatio = (c1: string, c2: string, c3: string) => { // step1: それぞれのRGB値を計算する const [r1, g1, b1] = getRGB(c1); const [r2, g2, b2] = getRGB(c2); const [r3, g3, b3] = getRGB(c3); // step2: それぞれの相対輝度を計算する const L1 = getRelativeLuminance(r1, g1, b1); const L2 = getRelativeLuminance(r2, g2, b2); const L3 = getRelativeLuminance(r3, g3, b3); // step3: c1とc2とのコントラスト比とc1とc3とのコントラスト比を計算する const contrast1 = getContrastRatio(L1, L2); const contrast2 = getContrastRatio(L1, L3); // step4: どちらとのコントラスト比が高いか。高い方を返す。 return contrast1 > contrast2 ? c2 : c3; };
実装の細部は置いておくとして、戦略を直裁に書き下すとこのようになります。c1, c2, c3 は #ffffff
といったカラーコードの形状の文字列を想定しています。
次は getRGB
です。
const getRGB = (color: string) => { // color は '#ffffff' の形式 const c = color.substring(1, 7); const r = parseInt(c.substring(0, 2), 16); const g = parseInt(c.substring(2, 4), 16); const b = parseInt(c.substring(4, 6), 16); return [r, g, b]; };
あまり説明することがないですね。次は getRelativeLuminance
です。
// https://www.w3.org/TR/WCAG22/#dfn-relative-luminance const getRelativeLuminance = (R8bit: number, G8bit: number, B8bit: number) => { const RsRGB = R8bit / 255; const GsRGB = G8bit / 255; const BsRGB = B8bit / 255; const R = RsRGB <= 0.03928 ? RsRGB / 12.92 : ((RsRGB + 0.055) / 1.055) ** 2.4; const G = GsRGB <= 0.03928 ? GsRGB / 12.92 : ((GsRGB + 0.055) / 1.055) ** 2.4; const B = BsRGB <= 0.03928 ? BsRGB / 12.92 : ((BsRGB + 0.055) / 1.055) ** 2.4; return 0.2126 * R + 0.7152 * G + 0.0722 * B; };
これは relative luminance について調べる過程で実装例も一緒に見つけてしまったのでコピーぺしました。参考にしたコードもリンクさせていただきます。https://gist.github.com/jfsiii/5641126
最後に getContrastRatio
です。
// https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio // コントラスト比の公式 // (L1 + 0.05) / (L2 + 0.05) // L1: lighter color の relative luminance // L2: darker color の relative luminance const contrastRatioW3C = (L1: number, L2: number) => L1 > L2 ? (L1 + 0.05) / (L2 + 0.05) : (L2 + 0.05) / (L1 + 0.05);
getContrastRatio
では明るい色をL1に代入し暗い色をL2に代入しなければならないので、最初にふたつの相対輝度(色の明るさ)を比較します。そして明るい方をL1として、暗い方をL2として計算します。
これですべての関数を実装し終え selectHigherContrastRatio
が完成しました!
冒頭の画像を思い出してください。色々な背景色について黒か白の文字色付けていく操作を完成した selectHigherContrastRatio
を使ってもう一度やってみます。
selectHigherContrastRatio('いろいろな背景色', '#ffffff', '#000000');
うん!なんだか良さそうです!特に不評だった黄緑と水色の文字色は黒になり、やはりアクセシビリティの観点で良くなかったと判明しました!
最後に注意点として、selectHigherContrastRatio
によって選ばれた色の組み合わせが必ずしもアクセシビリティの観点で良いというわけではないということです。
そういうものが欲しい場合は、改善として contrast ratio が 4.5:1 か 7:1 を越えるよう検証するコードをどこかに入れると良さそうですね。
おわり
WCAG に沿った決定をした結果、ユーザー体験が向上し、デザイナと実装者の両方の頭を悩ませる負担を軽くすることができました。
よかったですね。
トヨクモ株式会社ではUXに真摯に向き合いたいデザイナ、Clojureを書きたいエンジニア、PHP/Reactを書きたいエンジニア、技術が好きなエンジニアを募集しております。
よろしければ採用ページをご覧ください。