コンテナクエリは、親要素の特徴(幅や高さなど)をターゲットにして子要素にスタイルを適用するスタイル設定ロジックを記述できる新しい 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
条件は成立しなくなり、コンテンツが表示されます。 - コンテナクエリが組み込みでサポートされているブラウザでは、この条件は決して成立しないため、ページは想定どおりにファーストペイントで表示されます。
まとめ
古いブラウザでコンテナクエリを使用する場合は、ポリフィルをお試しください。問題が発生した場合は、お気軽に問題を報告してください。
皆様がこの機能を使って作成される素晴らしい作品を楽しみにしています。