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

Gerald Monaco
Gerald Monaco

コンテナクエリは、親要素の特徴(幅や高さなど)をターゲットにして子要素にスタイルを適用するスタイル設定ロジックを記述できる新しい CSS 機能です。最近、ブラウザでのサポート開始に合わせて、ポリフィル大きなアップデートがリリースされました。

この記事では、ポリフィルの仕組み、ポリフィルによって克服される課題、ポリフィルを使用して優れたユーザー エクスペリエンスを提供するためのベスト プラクティスについて説明します。

詳細

トランスピレーション

ブラウザ内の CSS パーサーは、まったく新しい @container ルールなど、不明な at ルールに遭遇すると、存在しなかったものとして破棄します。したがって、ポリフィルが最初に行う最も重要なことは、@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 が回帰する可能性があります。

訪問者に快適なエクスペリエンスを提供できるように、このポリフィルは、初回入力遅延(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 で表示されます。

まとめ

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

皆様がこの機能を使って作成される素晴らしい作品を楽しみにしています。