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は、いくつかの機能で動作します。
- Container Size Queries
- Container Query Length Units
- Container Style Queries for custom propreties
Container Size Queries と Container 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-type
と container-name
を同時に使用する時は container
プロパティにまとめて、下記のような書き方もできます。
.post {
container: post / inline-size;
}
便利そうに感じるContainer Queriesですが、注意点もあります。
container-type
か container-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
cqw
と cqi
は横幅に影響し、cqh
と cqb
は縦幅に影響します。
一見振る舞いは同じように見えますが、cqw
と cqh
は物理依存。
cqi
と cqb
は論理に依存しています。
このブログのように横書き、縦書きに対応している場合などは cqi
と cqb
。
通常のサイトであれば cqw
と cqh
で問題ないでしょう。
それでは使い方を見てみましょう。
.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-size
や border-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
など、様々なクラスの切り替えに使われそうな名前です。
しかし .card
に container-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-type
か container-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
からスタイルが反映されます。
ただ 480px
と 800px
のルールの位置が逆だと、困ります。
常時 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つです。
.anime
に animation
プロパティを設定すること。
[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の状態は解除さてしまいます。
そこで@container
で or
を使うことで、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