コンテナクエリ(Container Query)で真のレスポンシブを作る

profile image

メディアクエリでは解決が難しかったレスポンシブデザインの問題をコンテナクエリで解決する方法を学ぶ。ビューポートではなく親要素のサイズを基準にスタイルを適用し、真に再利用可能なコンポーネントを作ってみよう。

この記事は Jetbrains's Coding Agent Junie junie logoによって翻訳されました。誤訳があれば教えてください!

ほとんどの開発者が初めてレスポンシブを学び、作る際、メディアクエリ(Media Query)を利用して多く作ってきただろう。

レスポンシブウェブを作る際、メディアクエリを使用すればビューポートのサイズに応じてレイアウトを変更できる。しかし、開発をしているとビューポートではなくコンポーネントが実際に占める空間に応じてデザインが変わらなければならない時が多い。

例えば、同じコンポーネントが広いメイン領域に配置されることもあれば、狭いサイドバー領域に入ることもある。二つの領域が占める空間が異なるため、同じデザインを適用することはできず、狭い領域ではレイアウトが崩れる可能性が高い。メディアクエリではこのような状況を適切に扱うのが難しい。このような場合にコンテナクエリを使用すれば、親要素のサイズに反応する真に再利用可能なコンポーネントを作ることができる。

メディアクエリの限界

上記で例に挙げた状況をもう少し具体的に見てみよう。

メディアクエリはどのように動作するか?

メディアクエリはビューポートのサイズを基準にスタイルを適用する。

css
/* ビューポートの幅が768px以上の時 */
@media (min-width: 768px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

このコードは「画面が広ければカードを横向きに表示する」というロジックだ。ビューポート中心の設計では十分に見えるが、コンポーネント中心の設計では問題が発生する。

実際に直面する状況

同じコンポーネントが複数の場所で異なるサイズで使われる時

ニュースレター購読コンポーネントがある。デスクトップ(1280px)ではインプットとボタンが横に配置され、それより小さい時は縦に配置されるようにしてある。このコンポーネントをデスクトップ画面で広い領域(800px)と狭い領域(300px)に配置するとしよう。

メディアクエリはビューポートだけを見るため、「1280pxより大きいので横レイアウトを適用しよう!」と判断し、その結果、狭い領域でも横レイアウトが適用されてコンポーネントが崩れることになる。

브라우저 크기를 늘려보세요.

メディアクエリの限界を整理すると次の通りだ。

  • ビューポートのみを基準に判断する: コンポーネントが実際に占める空間は知ることができない
  • コンポーネントの再利用性が落ちる: 同じコンポーネントを異なるサイズの領域に配置すると、意図とは異なって見えることがある
  • レイアウト変更に弱い: 親レイアウトが変われば子コンポーネントのスタイルも一緒に修正しなければならない

これらの問題をコンテナクエリで解決できる。

コンテナクエリとは?

コンテナクエリはメディアクエリとは異なり、ビューポートではなく親要素(コンテナ)のサイズを基準にスタイルを適用できるCSS機能だ。これにより、コンポーネントが実際に占める空間に応じて反応する真に再利用可能なコンポーネントを作ることができる。

コンテナクエリはサイズだけでなく、次のようなコンテナの状態を基準にスタイルを適用できる。

サイズクエリ (Size Queries)

  • コンテナの幅(width)、高さ(height)を基準にスタイル適用
  • 最も一般的に使用される方式

スクロール状態クエリ (Scroll State Queries)

  • コンテナのスクロール状態を基準にスタイル適用

簡単な例

最初に例に挙げた状況にコンテナクエリを適用してみよう。

css
.card-container {
  container-type: inline-size;
}

/* コンテナが400px未満の時: 縦レイアウト */
.card {
  display: flex;
  flex-direction: column;
}

/* コンテナ가 400px以上の時: 横レイアウト */
@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 180px 1fr;
  }
}
html
<div class="layout">
  <main class="main-content">
    <!-- メイン領域: 広いコンテナ -->
    <div class="card-container">
      <div class="card">...</div>
    </div>
  </main>

  <aside class="sidebar">
    <!-- サイドバー: 狭いコンテナ -->
    <div class="card-container">
      <div class="card">...</div>
    </div>
  </aside>
</div>

同じコンポーネントであるが、ビューポートではなくコンテナのサイズを基準にスタイルを適用するため、広い領域では横レイアウトに、狭い領域では縦レイアウトに自然に表示される。

Sidebar: OK

基本的な使い方

コンテナクエリを使用するには、まず基準となる親要素に container-type プロパティでコンテナタイプを指定する必要がある。

css
/* Keyword values */
container-type: normal;
container-type: size;
container-type: inline-size;
container-type: scroll-state;

/* Two values */
container-type: size scroll-state;

/* Global Values */
container-type: inherit;
container-type: initial;
container-type: revert;
container-type: revert-layer;
container-type: unset;
  • normal: デフォルト値。サイズクエリは不可能でスタイルクエリのみ可能
  • size: インライン(横)とブロック(縦)方向のサイズをベースにする。
  • inline-size: インライン方向(一般的に幅)のサイズをベースにする。
  • scroll-state: スクロール状態をベースにする。

@container 規則でクエリを作成する

コンテナを指定したなら、@container at-ruleを使用してコンテナクエリを定義できる。メディアクエリの @media と類似した構文を使用する。

css
/* 基本構文 */
@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 180px 1fr;
  }
}

/* 最大幅 */
@container (max-width: 400px) {
  .card {
    flex-direction: column;
  }
}

/* 範囲指定 */
@container (min-width: 400px) and (max-width: 800px) {
  .card {
    padding: 20px;
  }
}

コンテナ名の指定

複数のコンテナがネストされている時は、container-name プロパティで特定のコンテナを指定できる。

css
/* コンテナに名前を付与 */
.card-container {
  container-type: inline-size;
  container-name: card;
}

.sidebar-container {
  container-type: inline-size;
  container-name: sidebar;
}

/* 特定のコンテナを対象にクエリ */
@container card (min-width: 400px) {
  .card-title {
    font-size: 1.5rem;
  }
}

@container sidebar (min-width: 300px) {
  .sidebar-content {
    padding: 16px;
  }
}

コンテナ名を指定しない場合、最も近い親コンテナを基準にクエリが適用される。

新しい単位

コンテナクエリが登場したことで、コンテナのサイズを基準にする新しい長さの単位も共に登場した。ビューポート単位(vw, vh)がビューポートサイズを基準にするように、コンテナクエリ単位はコンテナのサイズを基準にする。

コンテナクエリの長さの単位

基本単位

  • cqw: コンテナ幅の1% (container query width)
  • cqh: コンテナ高さの1% (container query height)
  • cqi: コンテナインライン方向のサイズの1% (container query inline-size)
  • cqb: コンテナブロック方向のサイズの1% (container query block-size)
  • cqmin: cqicqb のうち小さい方の値
  • cqmax: cqicqb のうち大きい方の値

ほとんどの場合、横書きモードでは cqi が幅を、 cqb が高さを意味する。

css
.card {
  /* コンテナが大きくなるほど余白も一緒に増加 */
  padding: 2cqi;
  gap: 1cqi;
}

スタイルクエリ (Style Queries)

コンテナクエリはサイズだけでなく、コンテナに適用されたスタイルの値もクエリできる。スタイルクエリを使用すると、CSSカスタムプロパティ(CSS変数)の値に応じて子要素のスタイルを動的に変更できる。

基本的な使い方

スタイルクエリは style() 関数を使用してコンテナのスタイル値を確認する。

css
/* コンテナにCSS変数を定義 */
.card-container {
  --theme: dark;
}

/* コンテナのスタイル値に応じてクエリ */
@container style(--theme: dark) {
  .card {
    background: #1a1a1a;
    color: #ffffff;
  }
}

@container style(--theme: light) {
  .card {
    background: #ffffff;
    color: #000000;
  }
}

おわりに

JavaScriptだけでなくCSSも毎年新しい機能が追加されている。以前はJavaScriptで複雑に実装しなければならなかったことを、今ではCSSだけでもできるようになった。そのため、CSSの新しい機能も引き続き関心を持って見守る必要がありそうだ。

コンテナクエリを使用すれば、本当に再利用可能なコンポーネントを作ることができる。同じコンポーネントがどこに入ろうとその空間に合わせて勝手に反応するから、はるかに便利だ。メディアクエリでは解決が難しかった問題がすっきりと解決される感じだ。

ただし、CSSはJavaScriptのようにポリフィルを使いにくい。そのため、実際にプロジェクトに適用する前にブラウザのサポート状況を必ず確認しなければならない。幸い、コンテナクエリは主要なブラウザでサポートされているが、スクロールクエリやスタイルクエリなどはまだ実験的段階にあるものが多いため、ターゲットブラウザをチェックし、必要であれば段階的に適用するのが良いだろう。


参照