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

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 条件は成立しなくなり、コンテンツが表示されます。
  • コンテナクエリが組み込みでサポートされているブラウザでは、この条件は決して成立しないため、ページは想定どおりにファーストペイントで表示されます。

まとめ

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

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