コンテナクエリの使用方法

最近、Chris Coyier 氏は、

コンテナクエリがすべてのブラウザ エンジンでサポートされるようになった今、開発者がコンテナクエリを使用する機会が増えないのはなぜでしょうか。

Chris の投稿には考えられるさまざまな理由が挙げられています(意識の欠如、古い習慣が困難になるなど)が、際立った特に理由があります。

今はコンテナクエリを使用したいが、古いブラウザをサポートする必要があるためできないと考えているデベロッパーもいます。

タイトルから推測できるように、古いブラウザをサポートする必要がある場合でも、ほとんどのデベロッパーは本番環境でコンテナクエリをすぐに使用できると考えています。この投稿では、そのためにおすすめのアプローチについて説明します。

実際的なアプローチ

現時点でコンテナクエリをコードで使用するものの、すべてのブラウザで同じエクスペリエンスを表示したい場合は、コンテナクエリをサポートしていないブラウザ向けに JavaScript ベースの代替機能を実装できます。

そこで問題になるのが、フォールバックをどの程度包括的にすべきかということです。

他のフォールバックと同様に、有用性とパフォーマンスのバランスを取ることが課題となります。CSS 機能の場合、API 全体をサポートできない場合があります(ポリフィルを使用しない理由をご覧ください)。とはいえ、ほとんどのデベロッパーが使いたい主要な機能セットを特定し、それらの機能のみに対してフォールバックを最適化することで、かなり多くのことを成し遂げることができます。

しかし、ほとんどのデベロッパーがコンテナクエリに求めている「コアセット」とは何でしょうか。この疑問に答えるには、多くのデベロッパーが現在メディアクエリを使用してレスポンシブ サイトを構築している方法を考えてみましょう。

最新のデザイン システムとコンポーネント ライブラリのほとんどは、モバイル ファーストの原則に基づいて標準化されており、事前定義されたブレークポイントのセット(SMMDLGXL など)を使用して実装されています。コンポーネントは、デフォルトで小画面でも適切に表示されるように最適化され、その後、固定された大画面幅をサポートするため、スタイルが条件付きでレイヤ化されます。(この例については、BootstrapTailwind のドキュメントをご覧ください)。

このアプローチは、ビューポート ベースのデザイン システムと同様に、コンテナベースのデザイン システムにも当てはまります。ほとんどの場合、デザイナーにとって重要なのは、画面またはビューポートのサイズではなく、配置されたコンテキストでコンポーネントが利用できるスペースの大きさだからです。つまり、ブレークポイントはビューポート全体に対して相対的に(ページ全体に適用される)のではなく、サイドバー、モーダル ダイアログ、投稿本文などの特定のコンテンツ領域に適用されます。

(ほとんどのデベロッパーが現在行っている)モバイルファースト、ブレークポイント ベースのアプローチの制約内で作業できる場合、そのアプローチにコンテナベースのフォールバックを実装する方が、すべてのコンテナクエリ機能を完全にサポートするよりもはるかに簡単です。

次のセクションでは、この仕組みを詳しく解説するとともに、既存のサイトに実装する方法を順を追って説明します。

仕組み

ステップ 1: @media ルールではなく @container ルールを使用するようにコンポーネント スタイルを更新する

この最初のステップでは、ビューポート ベースのサイズ設定ではなく、コンテナベースのサイズ設定のメリットがあると思われるサイト上のコンポーネントを特定します。

1 ~ 2 個のコンポーネントから始めてこの戦略がどのように機能するかを確認することをおすすめしますが、すべてのコンポーネントをコンテナベースのスタイル設定に変換するのもよいでしょう。この戦略の優れた点は、必要に応じて段階的に導入できることです。

更新するコンポーネントを特定したら、そのコンポーネントの CSS 内のすべての @media ルールを @container ルールに変更する必要があります。サイズ条件は同じままにできます。

定義済みのブレークポイントを CSS ですでに使用している場合は、そのままそのまま使用できます。事前定義のブレークポイントをまだ使用していない場合は、それらの名前を定義する必要があります(これについては、後ほど JavaScript で参照します。ステップ 2 をご覧ください)。

デフォルトで 1 列である .photo-gallery コンポーネントのスタイルの例を以下に示します。このコンポーネントは、MD ブレークポイントと XL ブレークポイントでそれぞれ 2 列と 3 列になるようにスタイルを更新します。

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;
}

/* Styles for the `MD` breakpoint */
@media (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Styles for the `XL` breakpoint */
@media (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

これらのコンポーネント スタイルを @media ルールから @container ルールに変更するには、コード内で検索と置換を行います。

/* Before: */
@media (min-width: 768px) { /* ... */ }
@media (min-width: 1280px) { /* ... */ }

/* After: */
@container (min-width: 768px) { /* ... */ }
@container (min-width: 1280px) { /* ... */ }

コンポーネントのスタイルを @media ルールからブレークポイント ベースの @container ルールに更新したら、次にコンテナ要素を設定します。

ステップ 2: HTML にコンテナ要素を追加する

前のステップでは、コンテナ要素のサイズに基づいてコンポーネント スタイルを定義しました。次に、@container ルールが基準となるサイズのコンテナ要素となる、ページ上の要素を定義します。

任意の要素を CSS でコンテナ要素として宣言するには、その container-type プロパティを size または inline-size に設定します。コンテナルールが幅ベースの場合、通常は inline-size を使用します。

次のような基本的な HTML 構造を持つサイトについて考えてみましょう。

<body>
  <div class="sidebar">...</div>
  <div class="content">...</div>
</body>

このサイトの .sidebar 要素と .content 要素をcontainersにするには、CSS に以下のルールを追加します。

.content, .sidebar {
  container-type: inline-size;
}

コンテナクエリをサポートするブラウザでは、この CSS さえあれば、前のステップで定義したコンポーネント スタイルを、どの要素が含まれるかに応じて、メイン コンテンツ領域またはサイドバーのいずれかに対して相対的に調整するために必要なものになります。

ただし、コンテナクエリをサポートしていないブラウザの場合は、追加の作業が必要になります。

コンテナ要素のサイズの変化を検出し、その変化に基づいて DOM を更新するコードを追加して、CSS がそれに対応できるようにする必要があります。

幸い、これを行うために必要なコードは最小限で済み、あらゆるサイトやコンテンツ領域で使用できる共有コンポーネントに完全に抽象化できます。

次のコードは、再利用可能な <responsive-container> 要素を定義しています。この要素は、サイズ変更を自動的にリッスンし、CSS がスタイル設定の基準となるブレークポイント クラスを追加します。

// A mapping of default breakpoint class names and min-width sizes.
// Redefine these as needed based on your site's design.
const defaultBreakpoints = {SM: 512, MD: 768, LG: 1024, XL: 1280};

// A resize observer that monitors size changes to all <responsive-container>
// elements and calls their `updateBreakpoints()` method with the updated size.
const ro = new ResizeObserver((entries) => {
  entries.forEach((e) => e.target.updateBreakpoints(e.contentRect));
});

class ResponsiveContainer extends HTMLElement {
  connectedCallback() {
    const bps = this.getAttribute('breakpoints');
    this.breakpoints = bps ? JSON.parse(bps) : defaultBreakpoints;
    this.name = this.getAttribute('name') || '';
    ro.observe(this);
  }
  disconnectedCallback() {
    ro.unobserve(this);
  }
  updateBreakpoints(contentRect) {
    for (const bp of Object.keys(this.breakpoints)) {
      const minWidth = this.breakpoints[bp];
      const className = this.name ? `${this.name}-${bp}` : bp;
      this.classList.toggle(className, contentRect.width >= minWidth);
    }
  }
}

self.customElements.define('responsive-container', ResponsiveContainer);

このコードは、DOM 内の <responsive-container> 要素のサイズ変更を自動的にリッスンする ResizeObserver を作成することで機能します。サイズ変更が定義済みのブレークポイント サイズのいずれかと一致する場合、そのブレークポイント名を持つクラスが要素に追加されます(条件が一致しなくなると削除されます)。

たとえば、<responsive-container> 要素の width が(コードで設定したデフォルトのブレークポイント値に基づく)768 ~ 1,024 ピクセルの場合、次のように SM クラスと MD クラスが追加されます。

<responsive-container class="SM MD">...</responsive-container>

これらのクラスを使用すると、コンテナクエリをサポートしていないブラウザ向けに代替スタイルを定義できます(ステップ 3: CSS に代替スタイルを追加するをご覧ください)。

このコンテナ要素を使用するように以前の HTML コードを更新するには、サイドバーとメイン コンテンツの <div> 要素を <responsive-container> 要素に変更します。

<body>
  <responsive-container class="sidebar">...</responsive-container>
  <responsive-container class="content">...</responsive-container>
</body>

ほとんどの場合、カスタマイズなしで <responsive-container> 要素を使用できますが、カスタマイズが必要な場合は、次のオプションを使用できます。

  • カスタム ブレークポイント サイズ: このコードは、デフォルトのブレークポイント クラス名と最小幅サイズのセットを使用しますが、これらは自由に変更できます。breakpoints 属性を使用して、要素ごとにこれらの値をオーバーライドすることもできます。
  • 名前付きコンテナ: このコードは、name 属性を渡すことで名前付きコンテナもサポートします。これは、コンテナ要素をネストする必要がある場合に重要になります。詳しくは、制限事項のセクションをご覧ください。

次の例では、両方の構成オプションを設定しています。

<responsive-container
  name='sidebar'
  breakpoints='{"bp1":500,"bp2":1000,"bp3":1500}'>
</responsive-container>

最後に、このコードをバンドルするときは、機能検出と動的な import() を使用して、ブラウザがコンテナクエリをサポートしていない場合にのみコードを読み込むようにしてください。

if (!CSS.supports('container-type: inline-size')) {
  import('./path/to/responsive-container.js');
}

ステップ 3: CSS に代替スタイルを追加する

この戦略の最後のステップは、@container ルールで定義されたスタイルを認識しないブラウザ用のフォールバック スタイルを追加することです。そのためには、<responsive-container> 要素で設定されるブレークポイント クラスを使用して、これらのルールを複製します。

先ほどの .photo-gallery の例の場合、2 つの @container ルールのフォールバック スタイルは次のようになります。

/* Container query styles for the `MD` breakpoint. */
@container (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Fallback styles for the `MD` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.MD) .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Container query styles for the `XL` breakpoint. */
@container (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* Fallback styles for the `XL` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.XL) .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

このコードには、@container ルールごとに、対応するブレークポイント クラスが存在する場合に、<responsive-container> 要素を条件付きでマッチングする同等のルールがあります。

<responsive-container> 要素に一致するセレクタ部分は、:where() 機能擬似クラス セレクタでラップされます。これにより、フォールバック セレクタの特異性を @container ルール内の元のセレクタの特異性と同等に保ちます。

各フォールバック ルールは @supports 宣言にもラップされます。これはフォールバックが機能するために厳密に必要なわけではありませんが、ブラウザがコンテナクエリをサポートしている場合はこれらのルールを完全に無視することを意味します。これにより、スタイル マッチングのパフォーマンスが全般的に向上します。また、ブラウザがコンテナクエリをサポートし、フォールバック スタイルを必要としないことがわかっている場合、ビルドツールや CDN はこれらの宣言を削除することもできます。

このフォールバック戦略の主なデメリットは、スタイルの宣言を 2 回繰り返す必要があるため、面倒でエラーが発生しやすいことです。ただし、CSS プリプロセッサを使用している場合は、@container ルールとフォールバック コードの両方を生成するミックスインに抽象化できます。Sass の使用例を次に示します。

@use 'sass:map';

$breakpoints: (
  'SM': 512px,
  'MD': 576px,
  'LG': 1024px,
  'XL': 1280px,
);

@mixin breakpoint($breakpoint) {
  @container (min-width: #{map.get($breakpoints, $breakpoint)}) {
    @content();
  }
  @supports not (container-type: inline-size) {
    :where(responsive-container.#{$breakpoint}) & {
      @content();
    }
  }
}

次に、このミックスインを作成したら、元の .photo-gallery コンポーネント スタイルを次のように更新して、重複を完全に排除します。

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;

  @include breakpoint('MD') {
    grid-template-columns: 1fr 1fr;
  }

  @include breakpoint('XL') {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

これですべての設定が完了し、

内容のまとめ

ここまでの内容をまとめましょう。ここでは、クロスブラウザ フォールバックを使用してコンテナクエリを使用するようにコードを更新する方法を示します。

  1. コンテナに対してスタイルを設定するコンポーネントを特定し、@container ルールを使用するように CSS の @media ルールを更新します。また、コンテナルールのサイズ条件と一致するブレークポイント名のセットを標準化します(まだ標準化していない場合)。
  2. カスタム <responsive-container> 要素の基盤となる JavaScript を追加してから、コンポーネントを相対的に配置するページのコンテンツ領域に <responsive-container> 要素を追加します。
  3. 古いブラウザをサポートするには、HTML の <responsive-container> 要素に自動的に追加されるブレークポイント クラスに対応する代替スタイルを CSS に追加します。同じスタイルを 2 回記述しなくても済むよう、CSS プリプロセッサのミックスインを使用することをおすすめします。

この戦略の優れた点は、1 回限りの設定コストがかかる点です。設定後は、新しいコンポーネントを追加してコンテナに相対的スタイルを定義するために、追加の作業は必要ありません。

実際の動作

これらすべての手順がどのように組み合わされているのかを理解するには、動作中のデモを見るのがおそらく最善の方法です。

コンテナクエリを操作するユーザーの動画デモサイトユーザーはコンテンツ領域のサイズを変更し、含まれるコンテンツ領域のサイズに基づいてコンポーネント スタイルがどのように更新されるかを示しています。

このデモは、2019 年(コンテナクエリが登場する前)に作成されたサイトの更新版です。真にレスポンシブなコンポーネント ライブラリを構築するためにコンテナクエリが不可欠である理由を説明するためのものです。

このサイトにはすでに多数の「レスポンシブ コンポーネント」のスタイルが定義されているため、ここで紹介した戦略を簡単なサイトで試すのもよいでしょう。実は更新はとても簡単で、元のサイトのスタイルを変更する必要はほとんどありませんでした。

GitHub でデモのソースコード全体を確認できます。フォールバック スタイルがどのように定義されているかは、特にデモ コンポーネントの CSS で確認できます。フォールバックの動作だけをテストしたい場合は、フォールバックのみのデモを行えば、そのバリアントだけが含まれるようになります。これは、コンテナクエリに対応しているブラウザでもかまいません。

制限事項と改善点

この投稿の冒頭で述べたように、ここで説明する戦略は、デベロッパーがコンテナクエリを利用する際に実際に関心を持つユースケースの大半でうまく機能します。

ただし、この戦略が意図的にサポートしようとしていない高度なユースケースもあります。次に説明します。

コンテナ クエリユニット

コンテナクエリの仕様では、いくつかの新しい単位が定義されています。これらはすべて、コンテナのサイズを基準とします。場合によっては役立つこともありますが、レスポンシブ デザインの大部分は、割合やグリッド レイアウト、Flex レイアウトなどの既存の手段で実現できる可能性があります。

ただし、コンテナ クエリユニットを使用する必要がある場合は、カスタム プロパティを使用して簡単にサポートを追加できます。具体的には、コンテナ要素で使用される各ユニットに対して、次のようにカスタム プロパティを定義します。

responsive-container {
  --cqw: 1cqw;
  --cqh: 1cqh;
}

そして、コンテナ クエリ ユニットにアクセスする必要があるときはいつでも、ユニット自体を使用するのではなく、これらのプロパティを使用します。

.photo-gallery {
  font-size: calc(10 * var(--cqw));
}

次に、古いブラウザをサポートするには、ResizeObserver コールバック内のコンテナ要素で、これらのカスタム プロパティの値を設定します。

class ResponsiveContainer extends HTMLElement {
  // ...
  updateBreakpoints(contentRect) {
    this.style.setProperty('--cqw', `${contentRect.width / 100}px`);
    this.style.setProperty('--cqh', `${contentRect.height / 100}px`);

    // ...
  }
}

これにより、JavaScript から CSS に値を実質的に「受け渡し」し、必要に応じて CSS(calc()min()max()clamp() など)を最大限に活用して値を操作できます。

論理プロパティと書き込みモードのサポート

以下の CSS の例の一部では、@container の宣言で width ではなく inline-size が使用されていることにお気づきでしょうか。新しい cqi ユニットと cqb ユニット(インライン サイズ用とブロックサイズ用)もあります。これらの新機能は、CSS が物理的なプロパティや値ではなく、論理的なプロパティと値に移行したことを反映しています。

残念ながら、Resize Observer などの API は依然として widthheight で値を報告するため、設計に論理プロパティの柔軟性が必要な場合は、ご自身で検討する必要があります。

getComputedStyle() などでコンテナ要素を渡すことで書き込みモードを取得することは可能ですが、それによってコストがかかるため、書き込みモードが変化したかどうかを検出する適切な方法がありません。

このため、サイト所有者が必要に応じて設定(および更新)できるライティング モード プロパティを <responsive-container> 要素自体に受け入れる方法をおすすめします。これを実装するには、前のセクションで示したのと同じアプローチに従い、必要に応じて widthheight を入れ替えます。

ネストされたコンテナ

container-name プロパティを使用すると、コンテナに名前を付けて、@container ルールで参照できます。名前付きコンテナは、コンテナ内にネストされたコンテナがあり、特定のルールが(最も近い祖先コンテナだけでなく)特定のコンテナのみに一致する必要がある場合に役立ちます。

ここで説明するフォールバック戦略では、子孫コンビネータを使用して、特定のブレークポイント クラスに一致する要素のスタイルを設定します。複数のコンテナ要素の祖先からの任意の数のブレークポイント クラスが特定のコンポーネントに同時に一致する可能性があるため、コンテナがネストされている場合、この問題が発生する可能性があります。

たとえば、ここでは .photo-gallery コンポーネントをラップしている 2 つの <responsive-container> 要素がありますが、外側のコンテナが内側のコンテナよりも大きいため、異なるブレークポイント クラスが追加されています。

<responsive-container class="SM MD LG">
  ...
  <responsive-container class="SM">
    ...
    <div class="photo-gallery">...</div class="photo-gallery">
  </responsive-container>
</responsive-container>

この例では、外側のコンテナの MD クラスと LG クラスが、.photo-gallery コンポーネントに一致するスタイルルールに影響を与えます。これは、最も近い祖先コンテナとしか一致しないため、コンテナクエリの動作と一致しません。

これに対処するには、次のいずれかを行います。

  1. 競合を避けるため、ネストするコンテナには必ず名前を付けてください。また、ブレークポイント クラスの先頭にそのコンテナ名を付加してください。
  2. フォールバック セレクタでは、子孫コンビネータではなく子コンビネータを使用します(ただし、制限が厳しいため)。

デモサイトのネストされたコンテナ セクションには、名前付きコンテナと、コード内で Sass ミックスインを使用して名前付きと無名の両方の @container ルールのフォールバック スタイルを生成する例が示されています。

:where()、カスタム要素、Resize Observer をサポートしていないブラウザの場合はどうなりますか?

これらの API は比較的新しいように見えるかもしれませんが、すべて 3 年以上にわたってすべてのブラウザでサポートされており、Baseline の一部で広く利用可能です。

したがって、サイト訪問者の大部分が、これらの機能のいずれかをサポートしていないブラウザを使用していることを示すデータがなければ、代替機能なしで自由に使用すべきではありません。

それでも、この特定のユースケースで起こりうる最悪のケースは、ごく一部のユーザーに対してフォールバックが機能しないことです。つまり、コンテナサイズに最適化されたビューではなく、デフォルトのビューが表示されます。

サイトの機能は引き続き動作しますが、それこそが重要な点です。

コンテナクエリをポリフィルしない方法を考えてみましょう。

CSS 機能はポリフィルが難しいことで知られています。通常、ブラウザの CSS パーサー全体を再実装し、JavaScript にロジックをカスケードする必要があります。その結果、CSS ポリフィルの作成者は多くのトレードオフを余儀なくされ、ほとんどの場合、多くの機能制限と大きなパフォーマンス オーバーヘッドが伴います。

こうした理由から、Google Chrome Labs の container-query-polyfill など CSS ポリフィルを本番環境で使用することは通常おすすめしません。この CSS ポリフィルはサポートが終了しており、主にデモを目的としたものです。

ここで説明するフォールバック戦略は制限が少なく、必要となるコードが少なく、あらゆるコンテナクエリのポリフィルよりもはるかに優れたパフォーマンスを発揮します。

古いブラウザに対応する代替機能を実装する必要さえあるのでしょうか。

ここで説明した制限事項に懸念がある場合は、そもそもフォールバックを実装する必要があるかどうかを検討することをおすすめします。結局のところ、こうした制限を回避する最も簡単な方法は、代替機能なしで機能を使用することです。正直なところ、多くの場合、これは非常に合理的な選択です。

caniuse.com によると、コンテナクエリは世界のインターネット ユーザーの 90% によってサポートされています。また、この投稿を読んでいる多くの人々にとって、コンテナクエリはユーザーベースに対してかなり高い数値であると考えられます。そのため、ほとんどのユーザーにはコンテナクエリ版の UI が表示されるということに留意することが重要です。10% のユーザーは エクスペリエンスが損なわれるわけではありませんこの戦略に従えば、最悪の場合、一部のコンポーネントについてはデフォルトのレイアウト、つまり「モバイル」レイアウトが表示されますが、これは世界の終わりではありません。

トレードオフを決断する際は、すべてのユーザーに一貫性があり、標準を下回るエクスペリエンスを提供する最小の共通要素アプローチをデフォルトにするのではなく、大半のユーザーに合わせて最適化することをおすすめします。

したがって、ブラウザがサポートされていないためにコンテナクエリを使用できないと想定する前に、実際にコンテナクエリを採用した場合のエクスペリエンスについて検討してください。このトレードオフは、代替手段がない場合でも、それだけの価値があります。

今後の展望

この投稿をお読みいただき、現在本番環境でコンテナクエリを使用できることと、サポートしていないブラウザが完全になくなるまで何年も待つ必要がないことを理解していただければ幸いです。

ここで説明する戦略は、多少の追加作業は必要ですが、ほとんどの人が自社のサイトで採用できるほどシンプルで明快であるべきです。とはいえ、もっと簡単に導入できる余地は十分にあります。1 つのアイデアとしては、さまざまな要素を 1 つのコンポーネントに統合し、特定のフレームワークやスタックに合わせて最適化し、すべての接着作業を処理するという方法があります。このようなものを作成されましたら、ぜひお知らせください。プロモーションをお手伝いいたします。

最後に、コンテナクエリ以外にも、すべての主要なブラウザ エンジンで相互運用可能な優れた CSS と UI 機能が多数用意されています。コミュニティとして、ユーザーがメリットを得られるように、これらの機能を実際に使用する方法を考えてみましょう。