記事を検索

メディアクエリに依存しないレイアウト設計

WEBにおけるレスポンシブ対応では、これまでメディアクエリが多用されてきました。
メディアクエリは便利ですが、画面幅と実際にコンテンツが配置されたエリアの幅の乖離が大きい時に問題が起こりやすくなります。

特に、サイドバーのあるページでは、画面幅は十分広くてもメインエリアが狭いため、カードコンテンツが横並びになる画面サイズなのに実際の表示は窮屈になっている、というようなことがよく起こってしまいます。

このページでは、そのようなメディアクエリの問題を回避するためのレイアウト手法をいくつか紹介していきます。

メディアクエリの問題点をおさらい

以下の例では画面幅 760px を境にして横並びへ切り替えています。

サイドバーがあると「760px以上なのにメインエリアは狭い」 という帯域が生まれるため、760pxより少し大きいくらいの画面サイズ付近で読みづらいレイアウトが発生していることが確認できます。

Demo Preview
別タブで表示 ↗
/* メインコンテンツ・サイドバーのレイアウト用スタイル */
@import "../_layout.css";


/* メディアクエリでレイアウトを切り替えるカード */
.mediaText {
	display: flex;
	flex-direction: column;
	gap: 1.5rem;
}

/* 大きい画面サイズは横並びにする */
@media (min-width: 760px) {
	.mediaText {
		flex-direction: row;
	}
	.mediaText .mediaText_image {
		flex-basis: calc(100% * 3/8);
	}
	.mediaText .mediaText_content {
		flex-basis: 60%;
		align-self: center;
	}
}

※ CSSが書かれていないクラスはLism CSSのものです。

(PCでリサイズして確認してみてください。)

この問題をメディアクエリで無理やり改善しようとすると、調整すべきブレイクポイントが増えてコードが肥大化していきます。

コンテナクエリに置き換えてみる

そこで有効なアプローチの一つが、コンテナクエリです。
コンテナクエリは、画面幅ではなく、要素が置かれたコンテナ(親要素)のサイズを基準にスタイルを切り替えられるため、より文脈に即したレスポンシブな設計が可能になります。

ページ全体がどういうレイアウトをしていようと関係ありません。コンポーネント単位で実装が完結するため、再利用性も高まります。

先ほどのメディアとテキストのレイアウトを切り替える例をコンテナクエリで実装してみましょう。

Demo Preview
別タブで表示 ↗
@import "../_layout.css";


/* コンテナの宣言 */
.main{
	container-type: inline-size;
}

/* メディアクエリでレイアウトを切り替えるカード */
.mediaText {
	display: flex;
	flex-direction: column;
	gap: 1.5rem;
}

/* 大きい画面サイズは横並びにする */
@container (min-width: 600px) {
	.mediaText {
		flex-direction: row;
	}
	.mediaText .mediaText_image {
		flex-basis: calc(100% * 3/8);
	}
	.mediaText .mediaText_content {
		flex-basis: 60%;
		align-self: center;
	}
}

※ CSSが書かれていないクラスはLism CSSのものです。

コードの変更点は以下の通りです。

.main{
container-type: inline-size;
}
/* 大きい画面サイズは横並びにする */
@media (min-width: 760px) {
@container (min-width: 600px) {
...

コンテナクエリの注意点

  • 先祖要素で container-type を設定していないと、コンテナクエリが機能しません。(コンテナ要素自身でコンテナクエリを使用することもできません)
  • コンテナ要素の直下では、flexboxやgridのサイズ感が少し変化します
  • コンテナ要素の内部では、position:fixedの挙動がabsoluteになります。(これは改善の方向に向かっているそうですが、まだモダンブラウザでも対応されたばかりです。)

他にも注意点がいくつかありますが、以下のスライドでよくまとまっていましたのでぜひ目を通してみてください。

俺流レスポンシブコーディング 2025 <a href="https://fortee.jp/fec-kansai-2025/proposal/0845f5b7-ee5c-4e94-a2c4-7c4f3716d4aa">フロントエンドカンファレンス関西 レギュラートーク</a>登壇資料 <h2>実装デモ</h2> https:/…
Speaker Deck

コンテンツ幅を基準にレイアウトを切り替える

コンテナクエリを使えば、ページのレイアウトに影響されてコンテンツが読みづらい範囲が出てきてしまう問題 を回避することができました。

めでたしめでたし。これで一安心です。

…と言いたいところですが、ここで少し考えたいことがあります。
親の幅に合わせてレイアウトを切り替えたい場面というのは、一定の表示幅を担保すべきコンテンツがそこにあるということです。

つまり本質的に重要なことは、「どれくらいの幅を最低限維持できていれば、そのコンテンツは見やすいんだろうか」ということです。

本来は、メディアクエリもコンテナクエリも使わずに、その幅を指定しておけば勝手にレイアウトが切り替わるようになっているべきなのです。

そこで、ぜひ一度目を通しておきたい書籍が、Every Layout です。

Relearn CSS layout
every-layout.dev

Every Layout は、無理に人間の手でレイアウトを調整しようとするのではなく、つまりメディアクエリを使わず、もっとシンプルにブラウザにレイアウトを任せようという考え方です。

If you find yourself wrestling with CSS layout, it’s likely you’re making decisions for browsers they should be making themselves. ( CSSのレイアウトで苦戦しているなら、それはおそらくブラウザ自身が決めるべきことをあなたが代わりに決めようとしているからです。 )

Every Layout

ここからは、この書籍の内容を参考に、Lism CSSにも取り入れている自動切り替えレイアウト手法を紹介していきます。

sideMain - コンテンツ幅を基準に2カラム↔︎1カラムを自動で切り替える方法

引き続き、「サイドバーの有無によってメインエリア内の横並びコンテンツが見づらくなる問題」を題材として、これをコンテナクエリも使わずに解決してみましょう。

やるべきこと
  • 要素が二つある。
  • 表示幅が広ければ横並びにする。
  • 表示幅が狭ければ縦並びにする。
  • 2つの要素のうち、可読性を維持したいテキストコンテンツがあり、そのコンテンツが最低限維持すべき幅を指定したい。

これを解決できるレイアウト手法が、Every Layout の中ではSidebarとして紹介されています。

ここではそれを元に少しだけ手を加えたl--sideMain というLism CSSで採用しているクラスを紹介していきます。

css
.l--sideMain {
/* --sideW, --mainW はあくまで初期値なので、その場その場で適切な値を指定して使います。 */
--sideW: auto;
--mainW: max(16em, 50%);
display: flex;
flex-wrap: wrap;
}
.l--sideMain > .is--side {
flex-basis: var(--sideW);
flex-grow: 1; /* 0 だと折り返されたときに広がらない */
}
.l--sideMain > :not(.is--side) {
flex-grow: 9999999; /* できるだけ fix側を 指定値ピッタリに近づけるためにかなり大きな数値を指定 */
flex-basis: min(100%, var(--mainW)); /* このサイズが折り返しポイントの基準となる */
}

これらのCSSにより、「親要素が何px以上なら横並び」と決めなくても、コンテナの幅に応じて自然に横並び/縦並びが切り替わるようになります。

CSSのポイント
  • .l--sideMainflex-wrapを指定し、横幅が足りなくなったら要素が自動的に折り返せるようにします。
  • .is--sideには、横並び時の理想的な幅(--sideW)をflex-basisで指定します。この時、flex-grow: 1を指定しておくことで、縦並びになったときには幅いっぱいまで広がるようにしておきます。
  • :not(.is--side)flex-basisのサイズをmin(100%, var(--mainW))として、最低限維持したい横幅(--mainW)を指定します。
  • :not(.is--side)flex-growにかなり大きな数値を指定しておくことで、横並び時に.is--side側が--sideWよりも広がらないようにする

実際に使用してみましょう。

l--sideMain では、2つの子要素をside要素とmain要素として分けて考えます。

  • side: サイドバーや画像、アイコンなど、ある程度表示したい幅が決まっている要素
  • main: 最低限維持したい横幅を指定するコンテンツ要素

今回の例では、画像がside要素、テキストとリンクのグループがmain要素となります。

Demo Preview
別タブで表示 ↗
/* メインコンテンツ・サイドバーのレイアウト用スタイル */
@import "../_layout.css";

.l--sideMain {
	/* --sideW, --mainW はあくまで初期値なので、その場その場で適切な値を指定して使います。 */
	--sideW: auto;
	--mainW: max(16em, 50%);

	display: flex;
	flex-wrap: wrap;
}
.l--sideMain > .is--side {
	flex-basis: var(--sideW);
	flex-grow: 1; /* 0 だと折り返されたときに広がらない */
}
.l--sideMain > :not(.is--side) {
	flex-grow: 9999999; /* できるだけ fix側を 指定値ピッタリに近づけるためにかなり大きな数値を指定 */
	flex-basis: min(100%, var(--mainW)); /* このサイズが折り返しポイントの基準となる */
}


/* メディアクエリ・コンテナクエリなしでレイアウトを切り替えるカード */
.mediaText.l--sideMain {
	gap: 1.5rem;

	--sideW: calc(100% * 3/8);
	--mainW: 24em;
}

※ CSSが書かれていないクラスはLism CSSのものです。

どうでしょうか。
メディアクエリもコンテナクエリも使わず、コンテンツの可読性を維持して自動でカラムが切り替わるのは素晴らしいですね。

fluidCols - カラム数の自動切り替えを実装する

続いて、カラム数が1列→2列→3列…と画面サイズに応じて順に変化する自動段組みレイアウトを紹介します。

こういったレイアウトはWEBでよく見かけますが、「440pxまでは1列、600pxから2列、800pxから3列…」といったようにブレークポイントを増やしがちで、CSSが肥大化する要因の1つとなります。

しかしGridレイアウトをうまく活用すると、「各カラムサイズで維持したい最低限のサイズ」を決めてあとはブラウザに列数を任せることができます。

Lism CSSでは、このレイアウトをl--fluidCols というクラスで提供しています。

CSS
.l--fluidCols {
--cols: 16rem; /* カラムが最低限維持する幅 */
--autoMode: auto-fit; /* auto-fill か auto-fit */
display: grid;
grid-template-columns: repeat(var(--autoMode), minmax(min(var(--cols), 100%), 1fr));
}

ポイントは以下の通りです。

  • grid-template-columnsrepeat()を使って、同じ幅でカラムを並べるよう指示します。
  • サイズ指定はminmax(最小サイズ, 1fr))の形で、各カードが最低限維持したい幅を指定します。
  • さらにこの時、最小サイズの部分はmin(サイズ,100%)とすることで、コンテナの幅を飛び越えてしまうことを防ぎます。
  • auto-fit または auto-fill を指定して、カラム数を自動で増減させます。
Demo Preview
別タブで表示 ↗
.l--fluidCols {
	--cols: 16rem; /* カラムが最低限維持する幅 */
	--autoMode: auto-fit; /* auto-fill か auto-fit */
	display: grid;
	grid-template-columns: repeat(var(--autoMode), minmax(min(var(--cols), 100%), 1fr));
}

※ CSSが書かれていないクラスはLism CSSのものです。

auto-fit と auto-fill の違い

auto-fillauto-fitはどちらも「自動的に列を増減する」ためのキーワードですが、1行あたりの想定列数より要素数が少ない場合(例:4列可能な幅の時に要素が3つしかない時)の隙間の埋め方に違いがでます。

  • auto-fit: 残りの空きカラムをなくし、要素が親要素いっぱいまで広がる。
  • auto-fill: 空きカラムはそのまま「空の列」として残る。

実際に両者を比較してみましょう。

auto-fill, --cols:160px
auto-fill
auto-fill
auto-fill, --cols:160px
auto-fit
auto-fit

auto-fill が隙間を埋めて広がるのは、1行で収まる時のみです。2列目以降で端数が出た時に広がることはありません。

2列目以降の挙動を確認
auto-fit
auto-fit
auto-fit
auto-fit
auto-fit

2列目以降も隙間を埋めて広げたい場合は、シンプルに flexboxレイアウトを活用します。

子要素: flex-basis:160px, flex-grow:1
Flex item
Flex item
Flex item
Flex item
Flex item

複数列以上↔︎1列の切り替えをメディアクエリを使わずに実装する方法

最後に、Every LayoutのSwitcherを紹介しておきます。

ここまで紹介したレイアウトだとできなかった、3列↔︎1列や4列↔︎1列の切り替えをメディアクエリやコンテナクエリを使わずに実装する方法です。

Demo Preview
別タブで表示 ↗

.switcher {
	display: flex;
	flex-wrap: wrap;
	--threshold: 30rem; /* 横並びを維持する幅(親のサイズで指定する) */
}

.switcher > * {
	flex-grow: 1;
	flex-basis: calc((var(--threshold) - 100%) * 9999);
}

※ CSSが書かれていないクラスはLism CSSのものです。

CSSのポイント
  • switcher自身の幅が--threshholdより大きな場合、flex-basis負の値となり、無視されます。その結果、flex-growだけが有効となり、各要素が等分になります。
    • この時、flex-growの値に差があれば、その比率に合わせて各要素が引き延ばされます。
  • switcher自身の幅が--threshholdより小さい場合、flex-basis が大きな正の値となるので、各要素は最大限に引き延ばされます。