Container Queriesの沼へようこそ

2023年2月13日にFirefoxでCSS Container Queriesが実装されたため、一部の機能がすべてのブラウザ使用できるようになりました。

さらに記事公開の本日、Google Chorome 111で、新しいContainer Queriesも実装されました。
私も待ち望んでいた機能なので、どう使うのか? どういう問題点があるのか?
解説していきます。

Container Queriesとは?

Container Queriesは、コンテンツにあわせてレイアウトを変化させる技術です。

例えば、写真付きのカードがあり、大きい画面では写真と説明が横に並ぶようなデザインがあったとします。
このような見た目の変化を従来のCSSではMedia Queriesを使用するしかなく、ブレイクポイントやクラスを付与して指定して変化させるしかありませんでした。

最初にこの問題に触れたのは normalize.cssにも関わっているJonathan Neal氏の Thoughts on Media Queries for Elements です。

この記事は normalize.css の Nicolas Gallagher が下記の言葉を発したことによって始まります。

We need native CSS media queries at the element/component/widget level, not just the viewport. Make it so, internetz.

私たちは、ビューポートだけでなく、要素/コンポーネント/ウィジェットレベルでネイティブのCSSメディアクエリが必要です。インターネットよ、実現してください。

そののちに初めて「Container Queries」という言葉が登場しました。
2015年のContainer Queries: Once More Unto the Breachという記事です。この頃はContainer Queriesは、まだ概念でした。
この記事を発端にContainer Queriesの議論がより活発になり、一部の機能が2023年に全ブラウザに実装されました。

Container Queriesの機能

Container Queriesは、いくつかの機能で動作します。

  1. Container Size Queries
  2. Container Query Length Units
  3. Container Style Queries for custom propreties

Container Size QueriesContainer Query Length Units は、現在全ての最新ブラウザでご利用できます。
ただ Container Style Queries for custom propreties に関しては、記事公開日時点でChrome 111しか対応していません。

Container Size Queries

Size queriesは、起点とする基準の要素を決定し、その要素サイズによって変化させます。
まずは例を見てみましょう。

.post {
  container-type: inline-size;
}

@container (min-width: 480px) {
  .post h2 {
    font-size: 3em;
  }
}

ここで使用しているCSSは container-type プロパティと @container ルールです。
container-type プロパティを指定された要素は Query Container になり、Container Queriesの基準になります。 そして @container ルールでは、Media Queriesのようにルールを設定して、どのサイズの時にどのような振る舞いを行うかを記述します。

もし、Size queriesを使用しつつ、特定の条件を追加したい場合は、container-name プロパティを使用しましょう。 container-name プロパティで設定した名前を @container に定義することで、コンテナをスコープ化できます。

.post {
  container-type: inline-size;
}
.post.-large {
  container-name: post-large;
}

@container (min-width: 480px) {
  .post h2 {
    font-size: 3em;
  }
}
@container post-large (min-width: 480px) {
  .post h2 {
    color: #faa;
  }
}

また container-typecontainer-name を同時に使用する時は container プロパティにまとめて、下記のような書き方もできます。

.post {
  container: post / inline-size;
}

便利そうに感じるContainer Queriesですが、注意点もあります。
container-typecontainer-name のプロパティを定義するとContainer Queriesの基準である Query Container になります。
Query Containerの 子孫要素 にしか @container は効きません。Query Containerになった要素は @container ルールに含められません。

ただしHTMLで .post の中に .post がある場合は、最初に指定された .post は無視されて、子孫の .post にスタイルが効くようになります。

.post {
  container-type: inline-size;
}

@container (min-width: 480px) {
  .post {
    border: 4px solid #000;
    padding: 1em;
  }
}

Size queriesは細かい制約がありますが、特定のサイズの依存だけを考えれば使いやすいでしょう。
基本的に、Media queriesの延長線上の考え方で使用できます。

Container Query Length Units

Container Queriesに関する新しい単位ができました。それが Container query length units です。

  • cqw : クエリコンテナの幅の1%
  • cqh : クエリコンテナの高さの1%
  • cqi : クエリコンテナのインラインサイズの1%
  • cqb : クエリコンテナのブロックサイズの1%
  • cqmin : いずれかの小さい値 cqi または cqb
  • cqmax : いずれかの大きい値 cqi または cqb

cqwcqi は横幅に影響し、cqhcqb は縦幅に影響します。
一見振る舞いは同じように見えますが、cqwcqh は物理依存。
cqicqb は論理に依存しています。

このブログのように横書き、縦書きに対応している場合などは cqicqb
通常のサイトであれば cqwcqh で問題ないでしょう。

それでは使い方を見てみましょう。 .box までは同じなのですが、 .box に対して .percent.container を追加しています。

<div class="wrap">
  <div class="inner">
    <div class="box percent">
      60%
    </div>
  </div>
</div>

<div class="wrap">
  <div class="inner">
    <div class="box container">
      60cqw
    </div>
  </div>
</div>
.wrap {
  container-type: inline-size;
  margin-top: 1em;
}
.inner {
  width: 80%;
}
.box {
  padding: 2em;
  border: 1px solid #000;
}
.percent {
  width: 60%;
}
.container {
  width: 60cqw;
}

数値は同じですが、単位によって .box のサイズが変化しています。
width プロパティのパーセント指定は、親のボックスモデルに依存します。
しかし、新しい cqw 単位を使用すると、Query Container (container-typeプロパティの指定箇所) 基準にサイズを決められます。

width プロパティで試しましたが、 font-sizeborder-width など本来コンテンツの幅から算出されないプロパティに対してもContainer queryの単位を付与できます。

できることもあれば、できないこともあります。
次の例を見てみましょう。

<div class="wrap">
  <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Mollitia rem voluptatibus ratione dolore veritatis porro dicta, vero iure aliquam. Sequi, numquam. Optio fugit repellat possimus velit laborum, dolore aliquam obcaecati.</p>

  <div class="box">
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum quae consequuntur labore saepe, neque veniam ducimus laboriosam error fugit quas eaque. Reprehenderit cum tenetur delectus aut laudantium eum eius illo!
  </div>
</div>
body {
  container: body / inline-size;
  margin: 0;
}
.wrap {
  max-width: 600px;
  margin: auto;
}
.box {
  margin-inline: calc(50% - 50cqw);
  padding-inline: calc(50cqw - 50%);
  padding-block: 2em;
  background: #333;
  color: #fff;
}

body 要素をQuery Containerに設定し、cqw を使用して .box のみを画面いっぱいに広げてみました。
これはうまくいきますが、次の section 要素を追加した例ではうまく動作しなくなります。

<div class="wrap">
  <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Mollitia rem voluptatibus ratione dolore veritatis porro dicta, vero iure aliquam. Sequi, numquam. Optio fugit repellat possimus velit laborum, dolore aliquam obcaecati.</p>

  <section>
    <div class="box">
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum quae consequuntur labore saepe, neque veniam ducimus laboriosam error fugit quas eaque. Reprehenderit cum tenetur delectus aut laudantium eum eius illo!
    </div>
  </section>
</div>
body {
  container: body / inline-size;
  margin: 0;
}
section {
  container: section / inline-size;
}
.wrap {
  max-width: 600px;
  margin: auto;
}
.box {
  margin-inline: calc(50% - 50cqw);
  padding-inline: calc(50cqw - 50%);
  padding-block: 2em;
  background: #333;
  color: #fff;
}

cqw などの単位は、直近のQuery Containerに依存します。そのため意図しないQuery ContainerがHTMLの間にくると、その要素の幅が基準に変わってしまいます。
これは container-name プロパティにも依存しないため、下記のように @container で名前空間を指定しても変化ありません。
Container queriesの単位を使用する時は、Query Containerが入れ子にならないように気をつける必要があります。

Container Style Queries for Custom Propreties

Style queriesは、Size queriesと同じように @container ルールを使用して記述しますが、先日リリースされたChrome 111のみで利用できる機能です。

style() 関数とCSS変数 (Custom propreties) を併用して使用します。

@container style(--demo: true) {
  .post {
    border: 1px solid #000;
  }
}

カードの例を作成しました。 .new を追加すれば、CSS変数が変更されるようになっており、擬似要素で「NEW」の文字が出ます。

<div class="card theme-new">
  <div class="card-header">
    <h2>Card Header</h2>
  </div>
</div>
.card {
  container-name: card;
  border-radius: 8px;
  box-shadow: 0 2px 1em #0004;
  padding: 1em;
}
.theme-new {
  --new: true;
}

@container card style(--new: true) {
  h2 {
    display: flex;
    align-items: center;
    gap: 0.5em;
  }
  h2:before {
    content: "NEW";
    color: #f66;
    font-size: 0.8em;
  }
}

ただ、よく見てください。
Style queriesの作用によってCSS変数でスタイルが切り変わっていますが、その状態変化はクラスで行なっています。
やっていることは従来の下記のコードよりも、文字数が増えただけのように感じます。

.theme-new h2 {
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.theme-new h2:before {
  content: "NEW";
  color: #f66;
  font-size: 0.8em;
}

この場合のStyle queriesのポイントは、CSS変数でスタイルが切り替わることではありません。
スコープです。

Style queries の Scope

Style queriesも container-name プロパティでQuery Containerの名前空間を定義できます。 そうするとその名前空間のQuery Containerの中だけで、スタイルが完結され、別のHTMLに影響を及ぼしません。

今回は .theme-new とだけ設定しています。
この状態だと .card.theme-new.article.theme-new , .box.theme-new など、様々なクラスの切り替えに使われそうな名前です。

しかし .cardcontainer-name: card とQuery Containerの名前空間を定義した上で @container card style(--new: true) と指定しているので、 card の名前空間ないの h2 要素のみにスタイルが適用されます。

さらに下記のようにすれば、要素セレクタで指定したとしても、他のHTMLを汚染することなく .theme-new のパターンを用意できます。

.card {
  container-name: card;
}
.article {
  container-name: article;
}
.theme-new {
  --new: true;
}

@container card style(--new: true) {
  h2:before {
    content: "NEW";
  }
}
@container article style(--new: true) {
  p:before {
    content: "NEW";
  }
}

単一のCSS変数で複数のコンポーネントの状態変化や --angle: left --angle: right のような指示方向によってスタイルを大きく変えたい場合などでも、Style queriesは活躍できそうです。

Style queriesの気をつけること

Style queriesもQuery Containerの影響します。
もしQuery Containerを設定しなくても、子孫の制限が発生します。

ボタンのコンポーネント例を見ましょう。

<button class="button theme-primary">Button</button>
.button {
  color: #000;
  background: #fff;
}
.theme-primary {
  --button-theme: primary;
}

@container style(--button-theme: primary) {
  .button {
    color: #fff;
    background: #f66;
  }
}

上記のコードで期待する挙動は .theme-primary でCSS変数が変わり、ボタンの見た目が変わることです。
しかし、このコードでは style() 関数がうまく動作しません。

Size queriesで container-typecontainer-name プロパティを指定された要素は Query Container になるとお伝えしました。
Style queriesのコードではどちらも指定していないため、Query Containerになっていませんが、CSS変数を定義した場所が問題です。

@container に書かれたスタイルのセレクタの直近の親を確認します。
その親の変数が style() 関数に一致しているかを確認します。
そのため自身の時にCSS変数の値を変更したとしても、CSSのパーサは親のCSS変数を確認しているので、スタイルが変更されないという状況になります。

下記のように .theme-primary を親に移動するとStyle queriesが正しく動作し、ボタンの見た目が変わります。

<div class="theme-primary"><button class="button">Button</button></div>

Container Queriesの沼(Style Queries)

Container Size Queriesで違うブレイクポイントを設定してほしい

Size Queriesを使ってコンポーネントにブレイクポイントを設定した、とします。
で、ある時デザイナー、もしくはクライアントから言われます。

「ここの箇所だけ、xxxのブレイクポイントの見え方にして欲しい」

なんかありそうなシチュエーションです。
この場合、どうすべきか考えてみます。

修正するのは下記のコードです。
特定の場所に入った時にブレイクポイントを変化させるように考えます。

<div class="component">
  <div class="component-box">
    Component
  </div>
</div>
.component {
  container-type: inline-size;
}
.component-box {
  color: #333;
  padding: 1em;
  font-size: 2rem;
}

@container (max-width: 480px) {
  .component-box {
    color: #fff;
    background: #f44;
    font-weight: 700;
  }
}

コンポーネントが入った時にブレイクポイントを変化させたい要素を仮に .irregular とします。 .irregular の中にコンポーネントが入った時に、CSS変数を true にしたものを用意し、@container も条件を追加します。

.irregular {
  --container-irrgular: true;
}

@container
  (max-width: 480px) or
  (style(--container-irrgular: true) and (max-width: 800px)) {
  ...
}

条件付きのグループルールがかなり長くなりましたが、これで条件を満たせました。
通常時は max-width: 480px でコンポーネントが変化し、 --container-irrgular が有効な時に max-width: 800px からスタイルが反映されます。

ただ 480px800px のルールの位置が逆だと、困ります。
常時 max-width: 800px が有効になってしまい style() 関数が無意味になります。

そこで @container にさらに条件を追加しましょう。
--container-irrgular というCSS変数は .irregular 以外には記述していませんが、CSS変数が見たらないない場合は initial を返します。
そのため下記の条件を追加するだけで、期待通りの動きになります。

@container 
  (style(--container-irrgular: initial) and (max-width: 800px)) or
  (style(--container-irrgular: true) and (max-width: 480px)) {
  ...
}

Container QueriesをCSS変数で操作したいけど

ブレイクポイントが1つだけであれば、既存技術でどうにかしていた古のContainer Queriesのハイブリッドもできそうだと思って、試してみました。
先ほどのコンポーネントの例を使って表現します。

この例でやろうとしていることは --breakpoint を設定し、その変数以下の時にStyle Queriesを反映させようとしています。
ブレイクポイントを変数で持つことによって、もしイレギュラーなブレイクポイントの変更があっても対応しやすいためです。

.component {
  --breakpoint: 480px;
  --query: min(100cqw - var(--breakpoint), 1px);
}

@container style(--query: 1px)  {
  ...
}

しかしこの記述は全く動作しません。
style() の中に入っているCSS変数を計算式ではなく、完全な文字列として認識しているからです。

style() 関数の中はComputed Styleを元に真偽判定を行います。
もし width: min(100cqw - var(--breakpoint), 1px) であれば、Computed Styleは 1px の文字列を返却します。
ただ変数の場合は、計算されずにComputed Styleを min(100cqw - var(--breakpoint), 1px) の文字列のまま返却します。

そのため上記のコードはうまく動作しません。
CSS変数の扱いに注意しましょう。

Container Queriesでアニメーションの領域を拡張する

複数の要素をCSSでアニメーションさせたい時は、animation プロパティをそれぞれ動かしたい要素に配置する必要がありました。
Style Queriesを使用すると、アニメーションのタイミングを一元管理できます。

準備として、動かしたいフレームで割った @keyframes を用意します。
そこにCSS変数を指定していきます。このCSS変数は animation プロパティから子孫に継承されます。

@keyframes anime {
  0% {
    --animation-query: 1;
  }
  33% {
    --animation-query: 2;
  }
  66% {
    --animation-query: 3;
  }
  100% {
    --animation-query: 4;
  }
}

次に動かしたい要素を用意します。
子がアニメーションで動かしたいものです。

<div class="anime">
  <div class="anime-item-1"></div>
  <div class="anime-item-2"></div>
  <div class="anime-item-3"></div>
  <div class="anime-item-4"></div>
</div>
.anime {
  container-name: anime;
  display: flex;
  gap: 1em;
  animation: anime 4s linear 0.3s infinite;
}
[class*=anime-item] {
  --i: 1;
  --color-hue: calc(var(--i) * 25);
  width: 48px;
  aspect-ratio: 1;
  background: lch(78 65 var(--color-hue));
  transition: translate 0.2s ease-out;
}
.anime-item-2 {
  --i: 2;
}
.anime-item-3 {
  --i: 3;
}
.anime-item-4 {
  --i: 4;
}

このコードでのポイントは2つです。

.animeanimation プロパティを設定すること。
[class*=anime-item]transition プロパティの設定です。
あとはStyle Queriesを使用して、フレームごとの要素の状態を決定していきます。

@container anime
  style(--animation-query: 2) or
  style(--animation-query: 3) {
  .anime-item-2 {
    translate: 0 64px;
  }
  .anime-item-3 {
    translate: -64px 0;
  }
}
@container anime
  style(--animation-query: 3) {
  .anime-item-1 {
    translate: 0 64px;
  }
  .anime-item-4 {
    translate: -192px 0;
  }
}

CSS変数の数値が切り替わっている間だけ、その状態が反映されます。
そのため次のフレームに移った時にStyle Queriesの状態は解除さてしまいます。

そこで@containeror を使うことで、CSS変数の状態をどこまで維持させるかフレームの指定ができます。 複雑なアニメーションを作成する時はJavaScriptを使用した方がいいですが、単純な複数要素のアニメーションにもStyle Queriesを利用できます。

最後に

冒頭でもいいましたが、Size Queries以外は全ての最新ブラウザで使えます。
Style Queriesは、今のところChromeだけです。

Size Queriesはいいとして、Style Queriesは扱いが難しいです。
一番使われる方法は、結局下記のデバッグコードじゃないかなと思っています。

:root {
  --debug: ;
}
/* :root もしくは特定の位置で `--debug: 1` すると
  それ以降のHTMLでデバッグ用の線が出ます */
@container style(--debug: 1) {
  *, *:before, *:after {
    outline: 2px solid #faaa;
    opacity: 1 !important;
    visibility: visible !important;
  }
}

ベータで試していたもの
完全体となったContainer Queriesの所感の雑記 - Qiita

CSS変数の扱いについて
Custom propertiesのセレクタの省略と詳細度の分離 - kojika17