コンテナクエリは、親要素の特徴(幅や高さなど)をターゲットにして子要素にスタイルを適用するスタイル設定ロジックを記述できる新しい 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% の cqw
や cqh
などです。これをサポートするため、CSS カスタム プロパティを使用して、単位が calc(...)
式に変換されます。ポリフィルは、コンテナ要素の行内スタイルを使用して、これらのプロパティの値を設定します。
.card { width: 10cqw; height: 10cqh; }
.card { width: calc(10 * --cq-XYZ-cqw); height: calc(10 * --cq-XYZ-cqh); }
インライン サイズとブロックサイズにそれぞれ cqi
や cqb
などの論理単位もあります。インライン軸とブロック軸は、クエリ対象の要素ではなく、単位を使用している要素の 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-type
や container-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 で表示されます。
まとめ
古いブラウザでコンテナクエリを使用する場合は、ポリフィルを試してみてください。問題が発生した場合は、お気軽に問題を報告してください。
皆様がこの機能を使って作成される素晴らしい作品を楽しみにしています。