コンテナ内のクエリ ポリフィル

Gerald Monaco
Gerald Monaco

コンテナクエリは新しい CSS 機能です。この機能を使用すると、親要素の機能(例: 幅、高さ)をターゲットにしたスタイル設定ロジックを記述して、子要素のスタイルを設定できます。最近、polyfill大規模なアップデートが、ブラウザでのサポートの開始と同時にリリースされました。

この投稿では、ポリフィルの仕組み、克服される課題、優れたユーザー エクスペリエンスを提供するためのベスト プラクティスをご紹介します。

詳細

トランスパイル

ブラウザ内の CSS パーサーは、まったく新しい @container ルールなど、未知の規則に遭遇した場合、存在しないかのようにそのまま破棄します。したがって、ポリフィルで最初に行う最も重要なことは、@container クエリを破棄しないものにトランスパイルすることです。

トランスパイルの最初のステップは、最上位の @container ルールを @media クエリに変換することです。これにより、ほとんどの場合、コンテンツがグループ化されたままになります。たとえば、CSSOM API を使用している場合や CSS ソースを表示する場合などです。

変更前
@container (width > 300px) {
  /* content */
}
変更後
@media all {
  /* content */
}

コンテナクエリが登場する前は、CSS には作成者がルールのグループを任意に有効または無効にする方法はありませんでした。この動作をポリフィルするには、コンテナクエリ内のルールも変換する必要があります。各 @container には独自の一意の ID(123 など)が割り当てられます。この ID は、この ID を含む cq-XYZ 属性が要素にある場合にのみ適用されるように各セレクタを変換するために使用されます。この属性は実行時にポリフィルによって設定されます。

変更前
@container (width > 300px) {
  .card {
    /* ... */
  }
}
変更後
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

:where(...) 疑似クラスを使用している点に注意してください。通常、追加の属性セレクタを含めると、セレクタの特異性が向上します。疑似クラスを使用すると、元の特異性を維持しながら追加の条件を適用できます。これが重要である理由について、次の例を考えてみましょう。

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

この CSS の場合、.card クラスの要素は常に color: red にする必要があります。これは、後のルールが常に同じセレクタと特異性で前のルールをオーバーライドするためです。したがって、最初のルールをトランスパイルして、:where(...) を使用せずに追加の属性セレクタを含めると、特異性が高くなり、color: blue が誤って適用されます。

ただし、:where(...) 疑似クラスはかなり新しく、サポートしていないブラウザには、ポリフィルで安全かつ簡単な回避策を提供します。@container ルールにダミーの :not(.container-query-polyfill) セレクタを手動で追加することで、意図的にルールの特異性を高めることができます。

変更前
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
変更後
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

これには次のようなメリットがあります。

  • ソース CSS のセレクタが変更されたため、特異性の違いが明確にわかります。また、回避策やポリフィルのサポートが不要になったときに、影響を受ける対象を把握するためのドキュメントとしても機能します。
  • ポリフィルでは変更されないため、ルールの特異性は常に同じです。

トランスパイル中、ポリフィルはこのダミーを同じ特異性を持つ属性セレクタに置き換えます。想定外の事態を避けるため、ポリフィルでは両方のセレクタを使用します。つまり、元のソースセレクタを使用して要素をポリフィル属性を受け取るかどうかを決定し、トランスパイルされたセレクタをスタイル設定に使用します。

疑似要素

では、ポリフィルで要素に cq-XYZ 属性を設定して一意のコンテナ ID 123 を含めると、属性を設定できない疑似要素をどのようにサポートできるのか、疑問に思われるかもしれません。

疑似要素は常に、DOM 内の実際の要素(開始要素)にバインドされます。トランスパイル中、条件セレクタは代わりにこの実要素に適用されます。

変更前
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
変更後
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

条件付きセレクタは、無効になる #foo::before:where([cq-XYZ~="123"]) に変換される代わりに、元の要素 #foo の最後に移動されます。

それだけではありません。コンテナは、その中に含まれないものを変更できません(また、コンテナをそれ自体の中に置くことはできません)。#foo 自体がクエリ対象のコンテナ要素であった場合、これはまさにどうなるでしょうか。#foo[cq-XYZ] 属性が誤って変更され、すべての #foo ルールが誤って適用されます。

これを修正するために、ポリフィルでは実際には 2 つの属性を使用しています。1 つは親によってのみ要素に適用できる属性、もう 1 つは要素自体に適用できる属性です。後者の属性は、疑似要素を対象とするセレクタに使用します。

変更前
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
変更後
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

コンテナは最初の属性(cq-XYZ-A)をコンテナ自体に適用しないため、最初のセレクタは、別の親コンテナがコンテナの条件を満たして適用された場合にのみ一致します。

コンテナ相対単位

コンテナクエリには、CSS で使用できる新しい単位も追加されています。たとえば、最も近い親コンテナの幅と高さの 1% の cqwcqh などです。これをサポートするため、CSS カスタム プロパティを使用してユニットを calc(...) 式に変換します。ポリフィルは、コンテナ要素のインライン スタイルを使用してこれらのプロパティの値を設定します。

変更前
.card {
  width: 10cqw;
  height: 10cqh;
}
変更後
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

インライン サイズとブロックサイズにそれぞれ cqicqb などの論理単位もあります。これらはやや複雑です。インライン軸とブロック軸は、クエリ対象の要素ではなく、ユニットを使用する要素writing-mode によって決定されるためです。これをサポートするため、ポリフィルは writing-mode が親と異なる要素にインライン スタイルを適用します。

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

これで、ユニットを以前と同様に適切な CSS カスタム プロパティに変換できるようになりました。

プロパティ

コンテナクエリでは、container-typecontainer-name などの新しい CSS プロパティもいくつか追加されます。getComputedStyle(...) などの API は不明なプロパティや無効なプロパティでは使用できないため、解析後に CSS カスタム プロパティにも変換されます。プロパティを解析できない場合(無効な値や不明な値が含まれているなど)、そのプロパティはブラウザが処理します。

変更前
.card {
  container-name: card-container;
  container-type: inline-size;
}
変更後
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

これらのプロパティは検出されるたびに変換されるため、@supports などの他の CSS 機能と適切に連携させることができます。この機能は、以下で説明するように、ポリフィルを使用する際のベスト プラクティスの基礎となります。

変更前
@supports (container-type: inline-size) {
  /* ... */
}
変更後
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

デフォルトでは、CSS カスタム プロパティは継承されます。たとえば、.card の子はすべて --cq-XYZ-container-name--cq-XYZ-container-type の値を受け取ります。これは明らかにネイティブ プロパティの動作とは異なります。これを解決するため、ポリフィルはユーザー スタイルの前に次のルールを挿入します。これにより、別のルールによって意図的にオーバーライドされない限り、すべての要素が初期値を受け取るようになります。

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

ベスト プラクティス

ほとんどの訪問者は、コンテナクエリが組み込まれたブラウザをいち早く使用することが予想されますが、残りの訪問者にも優れたエクスペリエンスを提供することが重要です。

初期読み込みでは、ポリフィルでページをレイアウトする前に、さまざまな処理を行う必要があります。

  • ポリフィルを読み込んで初期化する必要があります。
  • スタイルシートを解析し、トランスパイルする必要があります。外部スタイルシートの未加工のソースにアクセスする API は存在しないため、非同期的に再取得する必要がありますが、理想的にはブラウザ キャッシュからのみ再取得する必要があります。

このような懸念をポリフィルで慎重に対処しないと、Core Web Vitals が回帰する可能性があります。

訪問者に快適なエクスペリエンスを提供するため、ポリフィルは First Input Delay(FID)Cumulative Layout Shift(CLS)を優先するように設計されており、Largest Contentful Paint(LCP)が犠牲になる場合があります。具体的には、ポリフィルでは、コンテナクエリが初回ペイントの前に評価されるとは限りません。つまり、最適なユーザー エクスペリエンスを実現するには、コンテナクエリを使用してサイズや位置に影響するコンテンツをすべて、ポリフィルが読み込まれて CSS をトランスパイルするまで非表示にする必要があります。これを実現する 1 つの方法は、@supports ルールを使用することです。

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

これと、純粋な CSS 読み込みアニメーションを組み合わせて(非表示の)コンテンツに確実に配置して、何かが起こっていることをユーザーに知らせることをおすすめします。このアプローチの詳細なデモについては、こちらをご覧ください。

このアプローチは、次のような理由で推奨されています。

  • 純粋な CSS ローダは、新しいブラウザを使用するユーザーにとってのオーバーヘッドを最小限に抑え、古いブラウザや低速のネットワークを使用するユーザーに軽量なフィードバックを提供します。
  • ローダの絶対位置指定と visibility: hidden を組み合わせることで、レイアウト シフトを回避できます。
  • ポリフィルが読み込まれると、この @supports 条件が渡されなくなり、コンテンツが公開されます。
  • コンテナクエリのサポートが組み込まれたブラウザでは、この条件は満たされないため、ページが想定どおり First-Paint で表示されます。

まとめ

古いブラウザでコンテナクエリを使用する場合は、polyfill をお試しください。問題が発生した場合は、お気軽に問題を報告してください。

皆様がこのサービスを使って皆様が構築される素晴らしいサービスを拝見するのを楽しみにしております。

謝辞

ヒーロー画像作成: Dan Cristian P PasturedeveloperUnsplash