CSS の新機能であるコンテナクエリを使用すると、親要素の対象物(幅や高さなど)を対象とするスタイル設定のロジックを作成して、子要素のスタイルを設定できます。先日、polyfillの大幅なアップデートがリリースされました。これは、ブラウザへのサポートの導入と同時並行で対応しています。
この投稿では、ポリフィルの仕組み、解決される課題、優れたユーザー エクスペリエンスを提供するためのベスト プラクティスをご紹介します。
仕組み
トランスパイル
ブラウザ内の 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 のセレクタが変更されたため、詳細度の違いがはっきりとわかります。また、回避策やポリフィルのサポートが不要になった場合に影響を受けるものを把握できるよう、ドキュメントとしても役立ちます。
- ポリフィルでは変更されないため、ルールの具体性は常に同じになります。
トランスパイルの際、ポリフィルはこのダミーを同じ仕様の属性セレクタに置き換えます。想定外の事態を避けるため、ポリフィルでは両方のセレクタを使用します。元のソースセレクタを使用して要素が polyfill 属性を受け取るかどうかを決定し、トランスパイルされたセレクタはスタイル設定に使用されます。
擬似要素
疑問に思われるかもしれませんが、ポリフィルが要素に 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 カスタム プロパティに変換できます。
Properties
コンテナクエリには、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 の回帰につながる可能性があります。
訪問者に快適なエクスペリエンスを提供できるよう、ポリフィルは First Input Delay(FID)と Cumulative Layout Shift(CLS)を優先するよう設計されていますが、Largest Contentful Paint(LCP)が犠牲になる可能性があります。具体的には、ポリフィルでは、First Paint の前にコンテナクエリが評価される保証はありません。つまり、最適なユーザー エクスペリエンスを実現するには、コンテナクエリを使用してサイズや位置の影響を受けるコンテンツを、ポリフィルの読み込みと CSS のトランスパイルが完了するまで非表示にする必要があります。これを実現する 1 つの方法は、@supports
ルールを使用することです。
@supports not (container-type: inline-size) {
#content {
visibility: hidden;
}
}
このアニメーションと、(非表示の)コンテンツの真上に配置された、純粋な CSS の読み込みアニメーションと組み合わせて、何かが発生していることを訪問者に知らせることをおすすめします。このアプローチのデモ全体については、こちらをご覧ください。
このアプローチが推奨される理由はいくつかあります。
- 純粋な CSS ローダーは、新しいブラウザを使用するユーザーのオーバーヘッドを最小限に抑え、古いブラウザや低速ネットワークのユーザーには軽量なフィードバックを提供します。
- ローダの絶対位置を
visibility: hidden
と組み合わせることで、レイアウト シフトを回避できます。 - ポリフィルが読み込まれると、この
@supports
条件の通過が停止し、コンテンツが表示されます。 - コンテナクエリのサポートが組み込まれているブラウザでは、条件が渡されることがないため、ページは最初のペイントで想定どおりに表示されます。
おわりに
古いブラウザでコンテナクエリを使用したい場合は、polyfillをお試しください。問題が発生した場合は、お気軽に問題を報告してください。
Google Cloud を使って構築される素晴らしいサービスを、ぜひ目にし、体験していただければ幸いです。
謝辞
ヒーロー画像(作成者: Dan Cristian P エンゲージメント)(出典: Unsplash)