ウェブ開発において、レスポンシブデザインはもはや標準です。長年にわたり、メディアクエリ(@media
)はビューポートサイズに基づいてスタイルを適用するための主要なツールでした。しかし、真のコンポーネントベースの設計には、親要素のサイズに基づいて反応するコンポーネントが必要です。この問題を解決するために登場したのがCSSコンテナクエリ(@container
)です。
しかし、コンテナクエリには重要な問題があります:メディアクエリがwindow.matchMedia()
を通じてJavaScriptから操作できるのに対し、コンテナクエリには同様のJavaScript APIが欠けているのです。
問題の詳細
メディアクエリとJavaScript
メディアクエリでは、JavaScriptから現在の状態を確認したり、状態変化に反応したりするためにwindow.matchMedia()
メソッドを使用できます:
// メディアクエリの現在の状態を確認 const isMobile = window.matchMedia('(max-width: 768px)').matches; // メディアクエリの変化を監視 const mediaQuery = window.matchMedia('(max-width: 768px)'); mediaQuery.addEventListener('change', (event) => { if (event.matches) { console.log('モバイルビューになりました'); } else { console.log('デスクトップビューになりました'); } });
このAPIは非常に強力で、UIの動的な変更や適応型のユーザーエクスペリエンスを実装する際に不可欠です。
コンテナクエリの現状
コンテナクエリは次のようなCSSで使用できます:
.container { container-type: inline-size; } @container (min-width: 400px) { .child { font-size: 2rem; } }
このコードは、.container
要素が400px以上の幅を持つ場合に、その中の.child
要素のフォントサイズを大きくします。
しかし、コンテナクエリの状態をJavaScriptから確認したり、変化を監視したりする標準的な方法が存在しません。これは以下のようなシナリオで問題となります:
- コンテナサイズに基づいてDOM要素を追加・削除する必要がある場合
- コンテナサイズの変化に応じてアニメーションを実行する場合
- コンテナクエリの状態に基づいてカスタムロジックを実行する必要がある場合
この問題を解決するために、HTMLElement
インターフェース上にmatchContainer()
または類似の名前の新しいメソッドを追加することが最善です。現時点でmatchContainer()
というメソッドは存在しませんが、.matchMedia()
のように振る舞うmatchContainer()
polyfillが公開されました!
.matchContainer()
Polyfill
match-container パッケージは、提案されている Element.matchContainer() APIのポリフィルであり、CSS機能の@containerクエリに対応するスクリプティングAPIです。これは@containerクエリにとって、Window.matchMedia()が@mediaクエリCSS機能に対するものと同様です。この機能についての議論はこちらで見ることができます。このポリフィルはコンテナサイズクエリとコンテナスタイルクエリの両方をサポートしています。
APIと使用方法
ポリフィルを使用するには、HTMLドキュメント内の<script>
要素でロードするか、import 'match-container'
を使用してメインスクリプトにインポートするだけです。このポリフィルにはエクスポートがありません。ポリフィルは、Element.prototypeにこの名前の関数が存在しない場合にのみ、.matchContainer()
関数を追加します。
.matchContainer()
Element.matchContainer(containerQueryString: string)
この関数はElement インスタンスで呼び出すことができます。containerQueryString引数は構文的に有効な@containerクエリ条件でなければならず、対応するCSSで@containerとルールブロック{ ... }
の間に入るすべての部分を含んでいる必要があります。この呼び出しはContainerQueryListオブジェクト(下記参照)を返します。
例:
.matchContainer("(width < 400px)")
は以下のCSSに対応します
@container (width < 400px) { ... }
.matchContainer("outer-container (width < 400px)")
は以下のCSSに対応します
@container outer-container (width < 400px) { ... }
.matchContainer("style(--my-property)")
は以下のCSSに対応します
@container style(--my-property) { ... }
.matchContainer("not style(--my-property)")
は以下のCSSに対応します
@container not style(--my-property) { ... }
.matchContainer("style(--themeColor: blue) or style(--themeColor: purple)")
は以下のCSSに対応します
@container style(--themeColor: blue) or style(--themeColor: purple) { ... }
ContainerQueryList
ContainerQueryListはEventTargetを継承しているため、イベントリスナーをサポートしています。
ContainerQueryListのインスタンスは2つのプロパティを公開しています:
- container: string -
.matchContainer()
に提供されたcontainerQueryString引数に対応します - matches: boolean - コンテナクエリがマッチするかどうかを確認するにはこのプロパティを使用します
matchesの状態の変化に反応するために、changeイベントのイベントリスナーを追加することができます。
const myElement = document.getElementById("my-element"); const containerWidthQuery = myElement.matchContainer("(width < 400px)"); containerWidthQuery.addEventListener( "change", (event: ContainerQueryListEvent) => { if (event.matches) console.log("The @container query matches currently"); else console.log("The @container query does not match currently"); } );
ContainerQueryListEvent
ContainerQueryListEventは、ContainerQueryListのchangeイベントによって生成されるイベント引数の型です。2つのプロパティを公開しています:
- container: string -
.matchContainer()
に提供されたcontainerQueryString引数に対応します - matches: boolean - コンテナクエリがマッチするかどうかを確認するにはこのプロパティを使用します
動作の仕組み
Element.matchContainer()が呼び出されると、ポリフィルは対応する@containerクエリをCSSOMに挿入します。観察対象の要素は、対応するdata-*属性によってこのクエリブロック内の属性セレクターに紐付けられ、ターゲット要素がこのセレクターによってマッチする唯一の要素であることを保証します。@containerクエリがマッチすると、準備されたCSSカスタムプロパティが観察対象の要素に設定されます。スタイルオブザーバー(Bramus' StyleObserverから大きくインスピレーションを得ています)が最終的にスクリプトコード内のコールバックをトリガーします。
制限事項
このポリフィルは@containerクエリのスクリプティングAPI部分のみを追加し、CSS機能そのものをポリフィルするわけではないため、CSSで@containerクエリをサポートしていないブラウザでは動作しません。
フィードバック
issueやdiscussionを開いてフィードバックを提供することをためらわないでください。バグを見つけたり、ポリフィルが正常に動作していないと感じた場合は、ぜひお知らせください。私は認識しているバグのみを修正することができます。