Container Queriesという手法 / CSS Advent Calendar 2019

この記事は CSS Advent Calendar 2019 - Qiita 23日目の記事です。

Container Queriesを知っていますか?
Media Queriesに依存せず、コンテンツにあわせてレイアウトを変化させるを手法です。

Container Queriesとは

レスポンシブwebデザインを行う時、みなさんはMedia Queriesを使用していると思います。
ブレークポイントにあわせて、画面ごとにコーディングを進めていくのが一般的な方法でしょう。

その方法は本当に正しいのでしょうか?

Atomic Designやデザインシステムといった考え方が浸透してきて、UIなどをコンポーネントとして扱う場合も増えてきました。
しかし、コンポーネントとMedia Queriesは切り離せない関係です。ページの画面サイズに依存し、レイアウトを構築するする場合は、コンポーネントに何らかのブレークポイントの設定が必要なることも少なくないでしょう。

例えば、写真付きのカードがあり、大きい画面では写真と説明が並ぶようなデザインがあったとします。

上記のような見た目の変化を一般的なCSSでは、ビューポートにあわせて対応しなければなりません。
コンポーネントとして独立したパーツにしておきたいのに、レイアウトを変化させようと思うと、Media Queriesに頼らなければなりません。

これを良しせずに、コンポーネントが独立してレイアウトや色などを変化させる方法が模索されました。

Element Queriesの登場

まずは「Element Queries」という考え方が登場しました。
この考え方は、ビューポートをベースではなく、コンポーネントにブレークポイントを設定します。 下記がサンプルコードです。

.mod:media( min-width: 40em ) {
  width: 200px;
}

JSライブラリによってこの書き方が有効になり、コンポーネントは独立したパーツとして利用できるようになります。
ただし問題点もあります。

.our-element:media(min-width: 500px) {
  width: 499px;
}

要素サイズが「500px以上の時に499pxになる」と指定してみました。
この指定は矛盾が生じ、無限ループが発生します。

Container Queriesの登場

.mod:media( min-width: 320px ) .mod-cost {
  float: left;
}

この矛盾を解消するために「Container Queries」では、子の要素のみに影響を与えるように改良されました。上記の記述のみを良しとしたのです。

ただJSライブラリという言葉でお気づきの方も多くいらっしゃると思いますが、CSSに「:media()関数」はありません。
:media()関数は、Element QueriesやContainer Queriesの発案者が独自に定義して、JavaScriptで無理やり実装しています。「オレが考えた最強の構文」で、この問題を解決しようとしたのです。

以前にも独自CSS構文をJSでパースで表現する手法はありました。
しかし動的にCSSパースを行い、レンダリングを行うのはパフォーマンスに大きな影響を与えるので使い物になりません。
過去、多くの開発者が便利だと飛びつき、そしてCSSをパースするJSに地獄をみています。

Container Queriesを使うには?

CSSをパースするJSライブラリを使用するのは、よくないと言いましたが、Container Queriesの考え方は賛同できます。ビューポートベースではなく、コンテンツベースで切り替える手法。これはコンポーネントを作成する上で選択肢の一つになるでしょう。

Container Queriesの記事は、2015年に登場しました。現在もコンテンツ幅によって色や装飾を変化させることはできません。

しかしレイアウトは、新しいCSSプロパティの登場やトリッキーな手法が生み出されたことによって、ビューポートに左右されないContainer QueriesをCSSのみで実装できます。

Grid LayoutのContainer Queries

Grid Layoutは、Container Queriesのようなことが容易に実装できます。

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  grid-auto-rows: 1fr;
  gap: 16px;
}

この指定だけで、コンテンツ幅にあわせてカラムの数を変更できます。このレイアウトを支えているのは、minmax()関数とrepeat()関数の「auto-fit / auto-fill」です。

minmax()関数は、最小値と最大値を指定する関数です。

repeat()関数は、カラムを連続させる関数ですが、その関数の第1引数に、auto-fit もしくは auto-fill を指定します。
auto-fitは、親要素のスペースを埋めるようにグリッドアイテムの幅を変更します。
auto-fillは、親要素のスペースに空のグリッドを追加します。

デモのauto-fitとauto-fillのラジオボタンを変えて確認してみてください。 コンテンツ幅によるカラムの変化を体験できます。

Grid Layoutの注意

さっそくこの方法を使おうと思っている方がいたら、ちょっと待ってください。
この方法は便利方法なのですが、Internet Explorerに対応していません。スマートフォンかタブレットのサイトか、IE11が滅びてから使用してください。

FlexboxのContainer Queries

Grid Layoutの方法は、便利ですがIE11では使えません。
しかしFlexboxにも、Container Queriesの考え方に近い手法が生み出されており、IEでも利用できます。

手法の紹介前に、flexプロパティについて

Flexboxを使ったContainer Queriesの手法のポイントは、flexプロパティです。 手法の前に、このflexプロパティについて、おさらいしておきましょう。
flexプロパティは、「display: flex;」が指定されている子の要素に定義する一括指定プロパティです。値には flex-grow, flex-shrink, flex-basisプロパティを指定します。
それぞれのプロパティを簡単に説明していきましょう。

flex-growプロパティは、親要素の残りのスペースをどれだけ割り当てるか指定します。例えば全ての兄弟要素が「flex-grow: 1;」だった場合、等しく親要素のスペースが割り当てられます。

flex-shrinkプロパティは、親要素よりも子が大きい場合、どれだけ縮小するか決定します。0は全く縮小せず、0以上は、数値によって縮小していきます。

flex-basisプロパティは、初期の寸法を設定します。
これらのプロパティは、Container Queriesの基礎知識になるので覚えておきましょう。

Flex-grow 9999 Hack

.flex {
  display: flex;
  flex-wrap: wrap;
}
/* コンテナーのブレイクポイント
  240 + 400 = 640px */
.item-a {
  flex: 1 1 240px;
}
.item-b {
  flex: 9999 1 400px; 
}

flex-growプロパティは、残りのスペースをどれだけ割り当てるかを指定します。 そこでカラムの片方に「flex-grow: 9999;」を設定することで複数カラム時は「9999」が設定されているカラムの面を大きく保ち、カラム落ちが発生すると自動的に狭いカラムが広がります。
Media Queriesを使わずに、1カラムに変更できました。

では「flex-grow: 9999;」を使わなければ、どうなるでしょう?
HTMLの.item-aの要素を複製し、.item-bの要素を削除してみましょう。

.flex {
  display: flex;
  flex-wrap: wrap;
}
.item-a {
  flex: 1 1 240px;
}

全ての子要素のflex-growプロパティが統一され、同じ幅のまま行ごとのスペースを埋めるように振る舞います。 親要素の最大幅とflex-basisプロパティをうまく調節すれば、Grid Layoutとは違ったグリッドも作成できます。

写真付きカードをflex-basisプロパティで再現

.flex {
  display: flex;
  flex-wrap: wrap;
}
.image,
.box {
  flex: 1 1 320px;
}

冒頭で写真付きカードはMedia Queriesを使わなければ再現できないといいました。
しかし、flex-growプロパティの調整とflex-basisプロパティを使えばMedia Queriesは不要です。

デモでは、コンテンツ幅が640px以下になると1カラムになるように、flex-basisプロパティを変更しました。

1カラムから2カラムに

先ほどまで、広いコンテンツ幅は2カラム。狭いコンテンツ幅は1カラムに変更してました。

では、逆のことをしてみましょう。
広いコンテンツ幅は1カラム。狭いコンテンツ幅は2カラムに変更します。

.flex {
  display: flex;
  flex-wrap: wrap;
}

/* コンテナーのブレイクポイント
  320px * 2 = 640px */
.item {
  box-sizing: border-box;
  flex: 1 0 calc(100% - 320px);
}

この変更を行うポイントは、calc()関数とflex-basisプロパティを利用することです。
横幅に対して100%という値は、親のコンテンツ幅から算出されます。その値に固定幅で引いてあげると、固定値の総数がブレイクポイントになり、狭いコンテンツ幅で2カラムになります。

Container Queriesという名前

Container Queriesという名前を使って紹介しました。
しかし、コンテンツに応じたMedia Queriesを使わない手法は、これだけではありません。
Layout PrimitivesThe Fab Four techniqueといった似たような考え方や手法があります。

これらのMedia Queriesを使わない手法は積極的に取り入れるべき、と啓蒙しているわけではありません。レイアウトの構築方法に選択肢を明示しただけです。
Container QueriesもMedia Queriesも、レイアウト構築の単なる手段に過ぎません。
そして現在の技術ではレイアウトの柔軟性を考えると、Media Queriesに軍配があがります。

ただ実装要件によっては、Container Queriesの方が効率よくレイアウトを構築できる場面もあるでしょう。その時に思い出すぐらいが、ちょうどいいかもしれません。

関連
Container Queries: Once More Unto the Breach – A List Apart
レイアウトプリミティブ - シフトブレイン/スタンダードデザインユニット
[PR]真髄CSS レイアウト設計術 by kojika17