コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

CSSの相対カラー構文(RCS)を徹底活用!コンプライアンスと可読性を配慮した配色テクニック

On compliance vs readability: Generating text colors with CSS

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


私がデザインしたCSSの機能の中で、相対的な色(別名:相対的な色の構文、RCS)は間違いなく最も誇りに思うものの1つです。簡単に言えば、これはCSS作成者が既存の色の値から新しい色を導き出すことを可能にします。サポートされているあらゆる色空間で色成分に任意の計算を行うことができます:

--color-lighter: hsl(from var(--color) h s calc(l * 1.2));
--color-lighterer: oklch(from var(--color) calc(l + 0.2) c h);
--color-alpha-50: oklab(from var(--color) l a b / 50%);

エレベーターピッチは、より低レベルの操作を可能にすることで、色のバリエーションを導き出す方法について作成者に柔軟性を提供し、適切なより高レベルのプリミティブがどうあるべきかを理解するための時間を与えてくれるというものでした。

2024年5月現在、RCSはFirefox以外のすべてのブラウザでリリースされています。しかし、Interop 2024の重点分野であること、Firefoxが肯定的な標準的立場を表明していること、そしてBugzillaの問題に最近活動があり割り当てられていることから、近いうちにFirefoxでリリースされると楽観視しています(編集:この文を書いてから5日後、Firefox 128でリリースされました🎉)。私の予想では、2024年末までにベースラインになるでしょう。

私の予測が外れたとしても、すでに世界中のユーザーの83%が利用可能であり、caniuseのページを使用率でソートすると、残りの17%の大部分がFirefoxからではなく、古いChromeとSafariのバージョンからであることがわかります。現在の市場シェアを考えると、@supportsを使用して非対応ブラウザでも動作することを確認する限り(見た目は劣りますが)、今日の本番環境での使用に値すると思います。

ほとんどの相対的な色のチュートリアルは、その主要な使用ケースを中心に展開しています:特定の色成分を上下に調整したり、固定値で色成分を上書きしたりすることで、色合いや陰影、その他の色のバリエーションを作成することです。上記の例のようなものです。これは確かにいくつかの一般的な痛点に対処していますが、RCSが可能にするものの表面をかすかに引っ掻いているに過ぎません。この記事では、より高度な使用ケースを探求し、RCSの野生での創造的な使用を促進することを期待しています。

CSS contrast-color()関数

CSSの長年の大きな痛点の1つは、任意の背景色に対して読みやすいことが保証されるテキスト色を自動的に指定することが不可能だということです。例えば、暗い色には白、明るい色には黒といった具合です。

なぜそれが必要なのでしょうか?主な使用ケースは、色がCSS作成者の管理外にある場合です。これには以下が含まれます:

  • ユーザー定義の色。おそらく馴染みのある例:GitHubのラベル。ラベルを作成する際に任意の色を選択し、GitHubが自動的にテキスト色を選ぶ様子を思い浮かべてください。しばしば不適切な結果になります(理由は後ほど説明します)
  • 他の開発者によって定義された色。例えば、スタイリングのために特定のCSS変数をサポートするWebコンポーネントを作成している場合。テキストと背景に別々の変数を要求することもできますが、それによりWebコンポーネントの使いやすさが低下し、使用がより面倒になります。上書きできるが、めったに上書きする必要のないセンシブルなデフォルトを使用できたら素晴らしいと思いませんか?
  • Open Props、Material Design、あるいは(ギクッ)Tailwindのような外部のデザインシステムによって定義された色。

CSSコードの1行1行が単一の作成者によって管理されているコードベースでさえ、結合を減らすことでモジュール性を向上させ、コードの再利用を容易にすることができます。

良いニュースは、これがもはや長く痛点ではなくなるということです。CSS関数contrast-color()は、まさにこれに対処するために設計されました。これは新しいものではなく、以前にcolor-contrast()という名前で聞いたことがあるかもしれません。最近、私は最も顕著な痛点に対処し、実際に近いうちにリリースできるMVPにスコープを絞るコンセンサスを得ました。これにより、完全な機能を停滞させていたいくつかの非常に困難な設計上の決定を回避しています。その後、WGの決議に従って仕様に追加しましたが、いくつかの詳細はまだ詰める必要があります。

使用方法は次のようになります:

background: var(--color);
color: contrast-color(var(--color));

素晴らしいですね?もちろん、仕様の年数でいう「近いうちに」は、まだ何年もかかります。データポイントとして、私の過去の仕様作業を見ると、運が良ければ(そしてブラウザの関心があれば)、仕様が作成されてから主要なすべてのブラウザでリリースされるまでに最短で2年かかる可能性があります。標準化作業にも十分な資金が提供されている場合、機能が構想から全ブラウザでのベースラインまで2年で到達した例もあります。Cascade Layersがその代表例です:2019年10月にMiriamによって提案され、2022年3月にすべての主要ブラウザでリリースされました。しかし、2年でもまだ長い時間です(そしてもっと長くならない保証はありません)。それまでの間、私たちにはどのような選択肢があるでしょうか?

タイトルから推測できるかもしれませんが、答えはイエスです。見た目は良くないかもしれませんが、相対的な色を使用してcontrast-color()(またはそれに近いもの)をエミュレートする方法があります。

RCSを使用して自動的に対照的なテキスト色を計算する

以下では、CSSがサポートする最も知覚的に均一な極座標色空間であるOKLCh色空間を使用します。

  • 知覚的に均一な色空間:2つの色間のユークリッド距離がそれらの知覚的な差異に比例する色空間。RGB空間(およびその極形式であるHSL、HSV、HSB、HWBなど)は通常、知覚的に均一ではありません。これが何を意味するかの例については、LCHに関する私の古い投稿をご覧ください。知覚的に均一な色空間の例には、Lab、LCH、OkLab、OkLChなどがあります。
  • 極座標色空間:色が角度のある色相(「コア」の色、例えば赤、黄、緑、青などを決定する)と、その色相の正確な色合いを制御する2つの成分(通常、彩度と明度の何らかのバージョン)として表現される色空間。

色相と彩度に関係なく、黒テキストが読みやすいことが保証される明度値があり、それ以下では白テキストが読みやすいことが保証されると仮定しましょう。この仮定は後で検証しますが、今のところは当然のものとして扱います。この記事の残りの部分では、この値を閾値と呼び、Lthresholdとして表現します。

次のセクションでこの値をより厳密に計算します(そしてそれが実際に存在することを証明します!)が、今のところ0.7(70%)を使用しましょう。調整しやすくするために変数に割り当てることができます:

--l-threshold: 0.7;

野生のほとんどのRCS例では、単純な加算と乗算を使用したcalc()を使用しています。しかし、実際には、CSSでサポートされているあらゆる数学関数が公平に使用できます。これにはclamp()、三角関数、その他多くのものが含まれます。例えば、RCSを使用してコアカラーのより明るい色合いを作成したい場合、次のようなことができます:

background: oklch(from var(--color) 90% clamp(0, c, 0.1) h);

目的の結果から逆算してみましょう。広くサポートされているCSS数学関数で構成され、L ≤ Lthresholdの場合は1を、それ以外の場合は0を返す式を考え出したいと思います。そのような式を書くことができれば、その値を新しい色の明度として使用できます:

--l: /* ??? */;
color: oklch(var(--l) 0 0);

広くサポートされているCSS数学関数は次のとおりです:

  • calc()
  • min(), max(), clamp()
  • 三角関数(sin(), cos(), tan(), asin(), acos(), atan(), atan2())(2018年に私が提案したもう1つのCSS機能😁)
  • 指数関数(exp(), log(), log2(), log10(), sqrt())

タスクをどのように単純化できるでしょうか?1つの方法は、式が返す必要があるものを緩和することです。実際には正確に0または1を必要としません。L > Lthresholdの場合に0を、L ≤ Lthresholdの場合に> 1を与える式を見つけることができれば、clamp(0, //, 1)を使用して目的の結果を得ることができます。

1つのアイデアは比率を使用することです。比率には分子が分母よりも大きい場合は> 1、それ以外の場合は≤ 1という素晴らしい特性があります。

Lthreshold / L の比率は、L ≤ Lthresholdの場合は> 1、L > Lthresholdの場合は< 1です。これは、Lthreshold / L - 1 がL > Lthresholdの場合は負の数、L > Lthresholdの場合は正の数になることを意味します。あとは、その式に巨大な数を掛けて、正の数が確実に1を超えるようにするだけです。

すべてをまとめると、次のようになります:

--l-threshold: 0.7;
--l: clamp(0, (var(--l-threshold) / l - 1) * infinity, 1);
color: oklch(from var(--color) var(--l) 0 h);

Lが閾値に十分近づくと、0〜1の間の数が得られる可能性があるかもしれません。しかし、私の実験では、おそらく精度が有限であるため、これは決して起こりませんでした。

RCSをサポートしていないブラウザのためのフォールバック

パズルの最後のピースは、RCSをサポートしていないブラウザのためのフォールバックを提供することです。任意の色プロパティと任意の相対的な色の値をテストとして使用して、@supportsを使用できます。例えば:

.contrast-color {
  /* フォールバック */
  background: hsl(0 0 0 / 50%);
  color: white;

  @supports (color: oklch(from red l c h)) {
    --l: clamp(0, (var(--l-threshold) / l - 1) * infinity, 1);
    color: oklch(from var(--color) var(--l) 0 h);
    background: none;
  }
}

非対応ブラウザでも動作することを確認するという精神に基づいて、いくつかのフォールバックのアイデアを挙げると:

  • 白または半透明の白背景に黒テキスト、またはその逆。
  • テキスト色と反対の色の-webkit-text-stroke。これは太字のテキストでより効果的です。アウトラインの半分が文字の内側にあるためです。
  • テキスト色と反対の色の多数のtext-shadow値。これは細いテキストでより効果的です。テキストの背後に描画されるためです。

この神話的なL閾値は実際に存在するのか?

前のセクションでは、かなり大きな仮定をしました:色相と彩度に関係なく、黒テキストが読みやすいことが保証される明度値(Lthreshold)があり、それ以下では白テキストが読みやすいことが保証されるという仮定です。しかし、そのような値は実際に存在するのでしょうか?この主張を検証する時が来ました。

人々が最初にLab、LCH、またはそれらの改良版であるOkLab、OKLCHのような知覚的に均一な色空間について聞くとき、2つの色のコントラストを単にそれらのL(明度)値を比較することで推測できると想像します。残念ながら、これは真実ではありません。コントラストは知覚的な明度以外の要因にも依存します。しかし、明度値とコントラストの間には確かに重要な相関関係があります。

この時点で、ほとんどのWebデザイナーがWebコンテンツアクセシビリティガイドラインの一部であり、多くの国で法律に組み込まれているWCAG 2.1コントラストアルゴリズムを知っていることを指摘すべきです。しかし、これが非常に悪い結果を生み出すことは何年も前から知られています。非常に明るいまたは非常に暗い色以外の色に対しては、一部のテストでは、ランダムな判断とほぼ同じくらい悪い結果を示します。APCAという新しいコントラストアルゴリズムがあり、はるかに良い結果を生み出しますが、まだ標準や法律の一部ではなく、以前は一般に自由に利用できるようにするのに問題がありました(これはほぼ解決されたようです)。

Webの作成者はどうすればよいのでしょうか?実際のところ、かなりの窮地に陥っています。現時点でアクセシブルな色の組み合わせを作成する最良の方法は、2段階のプロセスのようです:

  1. APCAを使用して実際の読みやすさを確保する
  2. コンプライアンスのセーフガード:結果がWCAG 2.1に積極的に失格しないことを確認する

Color.jsを使用していくつかの簡単な実験を行いました。OKLCh参照範囲(大まかにP3ガムットに基づいています)を増加する粒度で反復し、白が「最良の」テキスト色(= 黒よりも高いコントラストを生成する)である色の明度範囲と、その逆の場合の範囲を計算しました。また、APCAとWCAGの両方について、各レベル(失格、AA、AAA、AAA+)の括弧も計算しました。

その後、この探索を対話型のプレイグラウンドに変換しました。ここでは、同じ実験を自分で実行できます。潜在的にはあなたのユースケースに合わせたより狭い範囲や、より高い粒度で実験できます。

これは、C ∈ [0, 0.4](ステップ = 0.025)および H ∈ [0, 360)(ステップ = 1)で生成されたテーブルです:

テキスト色 レベル APCA WCAG 2.1
最小 最大 最小 最大
最良 0% 75.2% 0% 61.8%
失格 71.6% 100% 62.4% 100%
AA 62.7% 80.8% 52.3% 72.1%
AAA 52.6% 71.7% 42% 62.3%
AAA+ 0% 60.8% 0% 52.7%
最良 66.1% 100% 52% 100%
失格 0% 68.7% 0% 52.7%
AA 60% 78.7% 42% 61.5%
AAA 69.4% 87.7% 51.4% 72.1%
AAA+ 78.2% 100% 62.4% 100%

これらは各レベルの最小および最大L値であることに注意してください。例えば、L ∈ [62.4%, 100%]の場合に白テキストがWCAGに失格する可能性があるという事実は、L > 62.4%のすべての色がWCAGに失格するという意味ではありません。単にいくつかがそうなるだけです。したがって、論理を逆にすることでのみ意味のある結論を導き出すことができます:すべての白テキストの失格がL ∈ [62.4%, 100%]を持つため、論理的にL < 62.4%であれば、色が何であれ白テキストはWCAGに合格することになります。

この論理をすべての範囲に適用することで、これらの括弧の多くについて同様の保証を導き出すことができます:

0%から52.7% 52.7%から62.4% 62.4%から66.1% 66.1%から68.7% 68.7%から71.6% 71.6%から75.2% 75.2%から100%
コンプライアンス WCAG 2.1
✅ AA ✅ AA |
✅ AA ✅ AAA ✅ AAA ✅ AAA ✅ AAA | ✅ AAA+
読みやすさ APCA
😍 最良 😍 最良 😍 最良 🙂 OK 🙂 OK |
🙂 OK 🙂 OK | 😍 最良

任意の色に対する黒と白のテキストのコントラスト保証を推論できます。OK = 合格しますが、必ずしも最良ではありません。

一般的に、WCAGは白テキストに関して多くの偽陰性があり、APCAよりも明度閾値をかなり低く設定する傾向があることに気付いたかもしれません。これはWCAGアルゴリズムの既知の問題です。

したがって、読みやすさとコンプライアンスのバランスを最適に取るには、できる限り高い閾値を使用する必要があります。これは以下を意味します:

  • WCAGに合格することが要件である場合、使用できる最高の閾値は62.3%です。
  • 実際の読みやすさが唯一の関心事である場合、WCAGを安全に無視し、68.7%から71.6%の間、例えば70%の閾値を選ぶことができます。

これらがどのように機能するかを確認できるデモをここに示します。以下の色を編集して、2つの閾値がどのように機能するかを確認し、カラーピッカーの横(または下)のテーブルに表示される実際のコントラスト括弧と比較してください。

明度 (0 – 1)

彩度 (0 – 0.4)

色相 (0 – 360)

Lthreshold = 70% Lthreshold = 64.5 % Lthreshold = 62.3% oklch(65% 0.12 180)

実際のコントラスト比 テキスト色 | APCA | WCAG 2.1 白 | AAA | AA 黒 | AA | AAA

"P3+"、"PP"、"PP+"とマークされた色を避けてください。これらはほぼ確実にあなたの画面のガムットの外にあり、ブラウザは現在適切にガムットマッピングを行わないため、視覚的な結果が正確ではありません。

上記の<color-picker>コンポーネントは、Color Elements(npmのcolor-elements)と呼ばれる新しいプロジェクトの一部です。これは、色関連のアプリやデモを簡単に作成できるようにする(非常に実験的な)Webコンポーネントのコレクションです。興味があれば、試してみてフィードバックを提供してください!

実際の色がより制限されている場合(例えば、色相や彩度のサブセット、または特定のガムット)、これらのトレードオフのバランスをよりよく取るために異なる閾値を使用できる可能性があることに注意してください。実際の色の範囲で実験を実行し、確認してください!

試してみた、より狭い範囲のいくつかの例と、WCAG 2.1に合格する最高の閾値を以下に示します:

説明 色の範囲 閾値
現代の低性能画面 sRGBガムット内の色 65%
現代の高性能画面 P3ガムット内の色 64.5%
将来の高性能画面 Rec.2020ガムット内の色 63.4%
中性色 C ∈ [0, 0.03] 67%
淡い色 C ∈ [0, 0.1] 65.6%
暖色(赤/オレンジ/黄色) H ∈ [0, 100] 66.8%
ピンク/紫 H ∈ [300, 370] 67%

現代の画面で実際に表示可能な色を無視するだけで閾値が64.5%に改善されるのは特に興味深いです。したがって、(残念ながら現在は成り立たない仮定ですが)ブラウザがガムットマッピング時に明度を優先的に保持すると仮定すれば、64.5%を使用してもWCAGコンプライアンスを保証できます。

これを異なる閾値と組み合わせることができるユーティリティクラスに変換することさえできます:

.contrast-color {
  --l: clamp(0, (var(--l-threshold, 0.623) / l - 1) * infinity, 1);
  color: oklch(from var(--color) var(--l) 0 h);
}

.pink {
  --l-threshold: 0.67;
}

結論 & 今後の課題

すべてをまとめて、フォールバックを含め、さらにcontrast-color()を使用する「フォールフォワード」も含めると、ユーティリティクラスは次のようになります:

.contrast-color {
  /* RCSをサポートしていないブラウザ用のフォールバック */
  color: white;
  text-shadow: 0 0 .05em black, 0 0 .05em black, 0 0 .05em black, 0 0 .05em black;

  @supports (color: oklch(from red l c h)) {
    --l: clamp(0, (var(--l-threshold, 0.623) / l - 1) * infinity, 1);
    color: oklch(from var(--color) var(--l) 0 h);
    text-shadow: none;
  }

  @supports (color: contrast-color(red)) {
    color: contrast-color(var(--color));
    text-shadow: none;
  }
}

これはただの始まりです。以下のような多くの改善の方向性が考えられます:

  • RCSは任意の色空間の任意の色成分で数学を行うことを可能にするため、CSSで実装可能でありながら、読みやすさとコンプライアンスのバランスをさらに良くとる、より優れた数式があるかもしれません。例えば、この記事を公開する直前にAndrew Somers(APCAの作成者)とチャットをしましたが、明度(L)の代わりに輝度(XYZのY成分)で数学を行うことが有望な方向性である可能性があることが示唆されました

  • 現在、白と黒のテキストに対してのみ閾値を計算しています。しかし、実際のデザインでは、純粋な黒のテキストを使用することはめったにありません。これが、contrast-color()が最大キーワードを使用しない限り、「非常に明るいまたは非常に暗い色」のみを保証する理由です。これを背景色のより暗い色合いに拡張するにはどうすればよいでしょうか?

補遺

しばしば起こることですが、このブログ記事を公開した後、多くの人々がこの分野に関連するさまざまな作業を共有してくれました。ここで最も興味深い発見のいくつかを紹介したいと思います。

明度の代わりに輝度を使用する

色の明度の値が十分に異なる場合(白または黒のテキストで起こるように)、人間は色彩コントラスト(色相/彩度が提供するコントラスト)を無視し、基本的に明度コントラストのみを使用して読みやすさを判断します。これが、Lが白または黒のテキストが最適に機能するかどうかの良い予測因子となる理由です。

もう1つの尺度である輝度は、基本的にXYZ色空間におけるYコンポーネントであり、黒テキストに切り替えるための良い閾値はY > 0.36です。これにより、テキスト色を計算するための別の方法が得られます:

--y-threshold: 0.36;
--y: clamp(0, (var(--y-threshold) / y - 1) * infinity, 1);
color: color(from var(--color) xyz-d65 var(--y) var(--y) var(--y));

Lloyd Kupchankのこのデモで見られるように、Ythreshold > 36%を使用すると、APCAによって決定された最適なテキスト色を非常に密接に予測します。

私のテスト(codepen)では、Lthreshold法と同様に機能しているように見えました。つまり、両者が一致しない色を見つけるのに苦労しました。しかし、このブログ記事の後、Lloydは彼のデモに様々なLthreshold境界を追加し、実際にLthresholdがAPCAと一致しない範囲がYthresholdよりも広いことが明らかになりました。

これを踏まえると、黒と白のテキスト間で切り替える必要がある場合はYthreshold法を使用し、テキスト色をさらにカスタマイズする必要がある場合(例えば、黒の代わりに非常に暗い色を使用する)はLthreshold法を使用することをお勧めします。

ブラウザのバグと回避策

この記事を公開してから約1週間後、color-mix()とRCSに関するブラウザのバグを発見しました。color-mix()を介して定義された色がfromで使用されると、RCSが無効になります。このテストケースを使用して、特定のブラウザが影響を受けているかどうかを確認できます。これはChrome 125とSafari TPリリース194で修正されましたが、色がどのように定義されているかを気にする必要がないというのがこの技術の全ポイントであるため、確かに問題を複雑にしています。

これを回避するには2つの方法があります:

  1. @supportsの条件をcolor-mix()を使用するように調整します:
@supports (color: oklch(from color-mix(in oklch, red, tan) l c h)) {
  /* ... */
}

欠点は、現時点ではこれが機能するブラウザのセットが非常に小さくなってしまうことです。

  1. 色を含むカスタムプロパティを登録します:
@property --color {
  syntax: "<color>";
  inherits: true;
  initial-value: transparent;
}

これは完全に修正されます。プロパティが登録されていれば、色がRCSに到達する頃には単に解決された色の値になるためです。@propertyは現在、RCSよりもはるかに広いセットのブラウザでサポートされているため、この回避策は互換性をまったく損ないません。

©コハム