Skip to content
On this page

ingred-ui contrast

2022-03-25


ingred-ui の Button コンポーネントのテキストカラーが白で決めうちだったので、ユーザーから指定された背景色によってテキストカラーを指定するという実装をしてみた。

既存の実装

該当箇所は以下。
https://github.com/voyagegroup/ingred-ui/blob/master/src/components/Button/Button.tsx#L42

既存の実装から見る。
通常のコンポーネントライブラリと同様、primary というベースの色をユーザーから受け取り、レンダリングする。デフォルトの色は青。

primary: {
  normal: {
    background: theme.palette.primary.main,
    color: theme.palette.text.white, // ここ
    boxShadow: `0px -2px ${hexToRgba(
      theme.palette.black,
      0.16,
    )} inset, 0px 2px ${hexToRgba(theme.palette.black, 0.08)}`,
    border: `1px solid ${theme.palette.primary.dark}`,
  },
...
1
2
3
4
5
6
7
8
9
10
11

これだと、例えば primary に white がきたら、ボタンの色とテキストの色が同じになってしまい、Button コンポーネントを呼ぶたびに明示的にテキストカラーを指定しないといけないということになる。

改善

Web Content Accessibility Guidelines (WCAG) 2.0 を参考にして書き直す。
単純な例として、背景色が黒ならテキストカラーは白、背景色が黒ならテキストカラーは白のように、ユーザーが指定した色から適切なテキストカラーをレンダリングする。

上記の実装を以下のように書き換えることを想定する。

primary: {
  normal: {
    background: theme.palette.primary.main,
    color: getContrastText(theme.palette.primary.main), // ここで背景色からテキストカラーを導いてレンダリングする
    boxShadow: `0px -2px ${hexToRgba(
      theme.palette.black,
      0.16,
    )} inset, 0px 2px ${hexToRgba(theme.palette.black, 0.08)}`,
    border: `1px solid ${theme.palette.primary.dark}`,
  },
...
1
2
3
4
5
6
7
8
9
10
11

このために、背景色から適切なコントラストを保持したテキストカラーを、背景色の輝度から導き、戻り値として渡すことができることを目標とする。

全体的な流れ

  1. 背景色を受け取る
  2. 受け取った背景色の輝度を見て、コントラストの比率を返す
  3. コントラスト比を見て、白 or 黒を渡す

getContrastText

まず、Button.tsx で呼ぶ用ための getContrastText 関数を定義する。
ここでは、背景色を引数として受け取り、それに合ったテキストカラーを返す。
ここでは、テキストカラーを返すだけで、色の判定自体は getContrastRatio 関数で実装している。

本番環境以外では、contrast の比率が 3:1 を切っている場合にエラーを返す。
Contrast (Minimum) に以下の記述がある。

Large Text: Large-scale text and images of large-scale text have a contrast ratio of at least 3:1;

少なくとも、コントラストは 3:1 を保っておくべきということで、ここで 3 未満の場合はエラーを出して正しいコントラストを保つようにしている。

export function getContrastText(background) {
  const contrastText =
    getContrastRatio(background, "#fff") >= 3 ? "#fff" : "#000";

  if (process.env.NODE_ENV !== "production") {
    const contrast = getContrastRatio(background, contrastText);
    // contrast が 3 未満だったらエラー
    if (contrast < 3) {
      console.log("error");
    }
  }

  return contrastText;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

getContrastRatio

ここでは、引数として渡された色の相対的な輝度を見て、コントラストの比率を返す。
比率は 1:1 ~ 21:1 の中で表現される。
ここで返された値を見て、getContrastText でテキストカラーを決めている。

ロジックは contrast ratio にある。

export function getContrastRatio(light, dark) {
  const lumA = getLuminance(light);
  const lumB = getLuminance(dark);
  return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
}
1
2
3
4
5

getLuminance

ここでは、色空間内の任意のポイントの相対的な輝度を返す。
最も暗い黒の場合は 0 に、最も明るい白の場合は 1 に正規化される。

ロジックは relative luminancedef にある。

ここまでくると、輝度を取得して、getContrastText 関数で背景色からテキストカラーを取得することが可能になる。

function getLuminance(color) {
  color = decomposeColor(color);

  let rgb =
    color.type === "hsl"
      ? decomposeColor(hslToRgb(color)).values
      : color.values;
  rgb = rgb.map((val: any) => {
    if (color.type !== "color") {
      val /= 255;
    }
    return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
  });

  return Number(
    (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3)
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

まとめ

所々省略しているところはあるが、w3c を参考にして考えてみた。
この手法は他のコンポーネントライブラリでも利用している例なので、ingred-ui でも背景色とテキストカラーが競合する可能性がある場合はこのロジックを利用してレンダリングしていきたい。