レスポンシブ実装の判断基準を明確にし、不要なブレイクポイントとコード量を最小化する。 CSS はブラウザへの「提案」であり、厳密な命令ではない。ブラウザに委ねられる仕事は委ねる。
レスポンシブ対応は以下の優先順序で検討する。上位で解決できるなら下位は使わない。 すべての要素がレスポンシブである必要はない。最初に「そもそもレスポンシブにすべきか」を判断する。
本質的に固定サイズの要素はレスポンシブにしない。
- タグ・バッジ・ラベルなど、内容量が少なく固定的な小さな要素
- アイコン(
1emでフォントサイズに追従するだけで十分) - 装飾的なボーダーや区切り線
- 固定サイズのアバターやロゴマーク
これらに clamp() やクエリを適用するのは過剰であり、コードの複雑さだけが増す。
クエリを一切使わず、要素自体が自然に適応する手法を最優先にする。
auto/fit-content/min-content/max-contentによる自然なサイジングflex-wrap: wrapによる自然な折り返しgrid+auto-fit/auto-fill+minmax()による自動カラム調整clamp()/min()/max()による流動的なサイジングmax-inline-sizeによる上限制約(inline-sizeの固定値ではなく)
レイアウトについては @layer composition で定義されている、柔軟でコンポーネントに依存しないレイアウトシステムの利用を優先する。
親要素のサイズに応じた切り替えが必要な場合に使用する。
使うべきケース:
- コンポーネントがサイドバー・モーダル・カード内など複数のコンテキストに配置される
- ビューポートではなく、配置先の幅に応じてレイアウトを変える必要がある
コンテナがネストされたとき、@container の参照先が曖昧になるのを防ぐため、container-name を必ず付与する。
/* 正しい — 名前付き */
:scope {
container: --cards / inline-size;
}
@container --cards (inline-size >= calc(420 / 16 * 1rem)) { ... }
/* 禁止 — 名前なし。ネスト時にどのコンテナを参照するか不明 */
:scope {
container-type: inline-size;
}
@container (inline-size >= calc(420 / 16 * 1rem)) { ... }container-name の名前は必ず dashed ident(-- で始まる名前)を使用する。
/* 正しい — dashed ident */
:scope {
container: --cards / inline-size;
}
@container --cards (inline-size >= calc(420 / 16 * 1rem)) { ... }
/* 禁止 — dashed ident ではない */
:scope {
container: cards / inline-size;
}
@container cards (inline-size >= calc(420 / 16 * 1rem)) { ... }理由:
- 近年の CSS では、ユーザー定義の名前を明確に区別するために
--を強制する流れがある(例:anchor-name、animation-timelineなど) - 「そのプロパティでは
--が必須か?」を個別に覚える必要が減る - 将来 CSS 標準側のキーワードが増えても、衝突リスクを下げられる
- 自分が定義した名前だと即座に判別できる
- コンポーネントルート、またはコンポーネント内で本当に必要な箇所のみに適用する
container-typeは後述する副作用が存在するため、「とりあえず全体にcontainer-typeを付ける」ことはしない.containerのようなユーティリティクラスは作ってはいけない
containerプロパティの登場によって、container はgridなどと同様に CSS 上で意味を持つワードとなったcontainerプロパティを定義している要素にのみ.***-containerのクラス名を使ってよい- 必ず具体性を持たせるようにする。
:scope以外では.sidebar-containerのように、何のコンテナかを名前で示す。.containerだけでは何のコンテナなのか分からない containerプロパティを持たない汎用ラッパーに.containerというクラス名は使わない
container-type: inline-sizeを指定した要素自身に対して、自身のサイズに基づく@containerクエリは適用されない。自己参照による無限ループのリスクがあるためである。コンテナの指定は親要素に行い、@containerクエリは子要素で使うcontainer-type: inline-sizeはインライン方向のコンテインメントを生成する。特にsubgridを指定する要素との併用は不可能である。grid の親要素をコンテナとし、subgridのコンテンツのブレイクポイントはcalc()を使用する。container-typeを指定した要素はcontain: inline-sizeが暗黙的に適用される。要素の幅が子のコンテンツに依存する場面(width: fit-content等)では要素の幅が極端に小さく計算され、サイズ崩壊が起こる。- 外部表示形式が
inlineの要素や Intrinsic な幅指定がされている要素にcontainer-typeを持たせてはいけない flex-growflex-basisが指定されていない flex アイテムや、autoや Intrinsic な grid アイテムにcontainer-typeが指定されていないか注意する
- 外部表示形式が
- 先述したように、暗黙的に
contain: layoutが適用される旧仕様が残っている環境では内包されるposition: fixedが破壊される。コンテナの内部でposition: fixedが適用された要素を含めないようにする container-typeを指定した要素にpadding/borderなど内部レイアウト関連のプロパティを指定しない。これらの値がコンテナのインラインサイズ計算に影響し、@containerクエリの閾値やcqiの計算がずれる原因になる。コンテナ要素は純粋なサイズ参照先として機能させ、レイアウトは子要素で行う<picture>自体を@container配下でスタイルすることはできるが、<source>のmedia属性はコンテナクエリを理解しない。画像ソース切り替えはビューポート基準のmediaに限定し、コンポーネント幅に応じた切り替えはマークアップ分岐や別実装で扱うcontainer-type: sizeはコンテナの高さが確約されている場合のみ有効化される。故に、min-block-sizeでは動作が行われず、扱いにくい指定である。原則的には指定しない方針とする
html/bodyやページ全体のレイアウト要素にcontainer-typeを指定してはいけない。ページレベルのレスポンシブはメディアクエリで行う- 現在は全コアブラウザで修正されているが、暗黙的に
contain: layoutが適用される旧仕様が残っている環境では内包されるposition: fixedが破壊される - CSSの仕様では禁止されていないが、コンテナクエリ提唱者の Miriam Suzanne 氏は自身の講演で
html/bodyやページ全体のレイアウト要素にcontainer-typeを指定をするアプローチをアンチパターンであると語っている
- 現在は全コアブラウザで修正されているが、暗黙的に
container-typeを単体で指定しない。必ずcontainerショートハンドで dashed ident のcontainer-nameを付けるようにする:scope以外でcontainer-typeを指定する場合は必ず.***-containerのようなクラス名を必須とする
ビューポート全体のレイアウト切り替えなど、上位の手法では対応できない場合にのみ使用する。
使うべきケース:
- ページ全体のマクロレイアウト(2カラム ↔ 1カラム)の切り替え
- ビューポートに固定された要素(モーダル、トースト、ポップオーバーされたコンテンツ)
- そのブロックがページに対して常に横幅100%であることが保証され、他のコンテキストに配置されることが無いと言い切れる場合(ヒーローヘッダーなど)
vw は使用してはいけない。接頭辞なしのビューポート単位は large viewport ベースに倒れるため、意図が曖昧で安全ではない。横方向は論理的指定の svi / dvi / lvi を文脈に応じて選ぶ。
svi: ブラウザUIが表示された最小インラインサイズでも安全に収めたい場合dvi: ブラウザUIの出入りに追従して、現在見えている領域へ合わせたい場合lvi: ブラウザUIが引っ込んだ最大インラインサイズを基準にしたい場合
原則的には svi を優先する。後述する cqi は基準コンテナが見つからない場合は svi を返すという仕様が存在し、それと合わせる。
vh も使用してはいけない。vh は large viewport ベースに倒れるため、モバイルブラウザのツールバー表示中に見切れや過大な高さを引き起こしやすい。縦方向は論理的指定の svb / dvb / lvb を文脈に応じて選ぶ。
svb: ブラウザUIが表示された最小ブロックサイズでも安全に収めたい場合dvb: ブラウザUIの出入りに追従して、現在見えている高さへ合わせたい場合lvb: ブラウザUIが引っ込んだ最大ブロックサイズを基準にしたい場合
原則的には svb を優先する。dvb はスクロールによってレイアウトシフトを引き起こすので避ける。背景画像を position: fixed で適用するなど、ブラウザUIの有無関係なく常に高さいっぱいにする場合は lvb を使用する。
ビューポート単位はページ全体の状態と結びつく。したがって、svi / dvi / lvi / svb / dvb / lvb は原則としてメディアクエリやページレベルのレイアウト判断と対で使う。
- ページ全体の余白、ヒーロー、全幅背景、ビュー全体に対して成立させたい流動値に使う
- コンポーネント単体の
inline-size/block-sizeやフォントサイズを、配置先と無関係にビューポート単位だけで決めない - コンポーネントが複数コンテキストへ再配置される可能性があるなら、ビューポート単位ではなくコンテナクエリを優先する
cqi / cqb / cqw / cqmin / cqmax は、名前付きのクエリコンテナが存在することを前提に使う。コンポーネントに雑に cqi を書いてはいけない。
cqiは descendant 側で使う。コンテナ自身の自己サイジングに使わないcqiを使う場合は、同じコンポーネント内にcontainer: --name / inline-sizeと@container --name (...)が存在するかを確認する- 「その要素に有効なクエリコンテナが必ずある」と言い切れない場所では
cqiを使わない cqb/cqw/cqmin/cqmaxはcontainer-type: sizeを適用しないと有効化されず、container-type: sizeは固定の高さを持つ必要があるため有効に利用できる場面はほぼ存在しない。原則的に使わない方針とする
有効な query container が無い場合、container query length units は small viewport 単位へフォールバックする。そのため、コンポーネントのベーススタイルに無造作に cqi を書くと、配置先によって意味が変わってしまう
svi/dvi/lvi/svb/dvb/lvbを直接使うと Chrome 系ブラウザのズーム時に拡大されずアクセシビリティに影響を及ぼす可能性がある- 原則的には
sv*/dv*/lv*は使わざるを得ないケースのみ使用し、%や Intrinsic なレイアウトで解決できる場合にはそれを優先する - プロジェクト全体でビューポート単位ベースの流動値を多用する場合は、都度生値を書くのではなく、トークンまたはユーティリティへ集約する
- Chrome 145 から条件下で
vw系がスクロールバーを含まずに計算される挙動に変わった- 具体的には
:rootにscrollbar-gutter: stableが適用されてスクロールバー分の余白が予約されている場合。当プロジェクトではkiso.cssを使用しており、そちらにこの指定がされているため、有効な環境下ではvw系はスクロールバーを含まずに計算される - 一方で Safari / Firefox では従来どおり横スクロールが発生し得るため、
inline-size: 100vwのような指定は避ける - そもそも
inline-size: 100vwは多くの場合「不要」または「別手段で置き換え可能」であり、全コアブラウザでスクロールバーを含まずに計算される挙動に置き換わったとしてもコードスメルを減らす目的で指摘対象になり得る
- 具体的には
- ヒーローやフルスクリーンUIで「少なくとも画面高いっぱい」を狙う場合も、
block-size固定よりmin-block-sizeを優先する。コンテンツ増加時のオーバーフローに強くなる
メディアクエリ・コンテナクエリともに、レンジ構文(>=, <=, >, <)を使用する。
min-width / max-width のプレフィックス構文は使用しない。
/* 正しい — レンジ構文 */
@media (width >= calc(720 / 16 * 1rem)) { ... }
@container --card (inline-size >= calc(420 / 16 * 1rem)) { ... }
/* 禁止 — プレフィックス構文 */
@media (min-width: calc(720 / 16 * 1rem)) { ... }
@container --card (min-width: calc(420 / 16 * 1rem)) { ... }コンテナクエリは論理的な方向指定が前提のため、width ではなく inline-size を使用する。
/* 正しい — 論理プロパティ */
@container --card (inline-size >= calc(420 / 16 * 1rem)) { ... }
/* 禁止 — 物理プロパティ */
@container --card (width >= calc(420 / 16 * 1rem)) { ... }カスケードレイヤーごとに、優先すべきレスポンシブ手法が異なる。
メディアクエリを優先する。ページ全体のマクロレイアウトはビューポートに依存するため。
コンテナクエリを優先する。コンポーネントはどのコンテキストに配置されるか予測できないため、ビューポートではなく親要素のサイズに基づいて適応すべき。 例外として、モーダルやトーストなど、ビューポートに依存するコンポーネントはメディアクエリを使用する。
2000 以上のデバイスサイズが存在する現在、sp / tablet / pc のようなカテゴリ分けは機能しない。
原則として sp / tablet / pc のようなデバイスを連想される命名は禁止とする。
レイアウトが崩れるポイントでブレイクポイントを設定する。
.sp-only / .pc-only のような表示切り替えクラスは使用してはいけない。
メディアクエリのブレイクポイントには rem を使用する。
フォントを拡大したユーザーは実質的に利用可能なスペースが減るため、rem にすることで適切にフォールバックする。
後述する calc() テクニックを使い、rem を使う際は @media (width >= calc(640 / 16 * 1rem)) のようにピクセル値を変換するように記述すること。
メディアクエリやコンテナクエリの条件、ならびにサイズ計算には calc() を積極的に使用する。
calc() は単なる四則演算のための関数ではなく、レイアウトの成立条件をコード上に明示し、マジックナンバーを減らすための手段である。
固定の数値をそのまま書くと、その値が何を根拠に決まっているのかが後から分からなくなる。
calc() を使い、以下のような意味のある値の組み合わせとして表現する。
- カラム最小幅 × カラム数
- アイテム幅 × 個数 + gap × 隙間数
- コンテンツ幅 + padding + border
- 基準サイズ同士の差分から導かれる流動値
これにより、48rem や 960px のような閾値を「たまたま決めた数値」ではなく、「そのレイアウトが成立するために必要な幅」として表現できる。
クエリの閾値は、デバイスカテゴリではなくコンテンツに基づいて決定する。
先に sp / tablet / pc のような分類を置くのではなく、「何が何個並ぶ必要があるか」「そのために何幅必要か」を calc() で組み立てて条件にする。
._card-group {
@container --card-group (calc(160 / 16 * 1rem * 6 + 24 / 16 * 1rem * 5) > inline-size >= calc(160 / 16 * 1rem * 4 + 24 / 16 * 1rem * 3)) {
& > :nth-child(2n + 1):nth-last-child(1) {
/* 奇数番目かつ最後の1つの要素を2列目から開始(中央寄せ) */
grid-column-start: 2;
}
}
}このように、閾値そのものではなく根拠を式として書く。
- クエリ条件に使う閾値は、可能な限り
calc()で構成要素へ分解して書く clamp()min()max()の内部でも式を使い、値の根拠を明示する- gap や padding を考慮する必要がある場合は省略せず式に含める
- 単位変換(px → rem)にも
calc()を原則として使用する - 値の意味が読み取りにくい場合は、コメントで「何列・何個・何本分なのか」を補足する
calc() はメディアクエリやコンテナクエリの条件式で使用できるが、その式の中で var() によるカスタムプロパティ参照を使ってはいけない。
var() はプロパティ値の置換に使うものであり、クエリ条件の構文要素としては使えない。
そのため、AI が出力しがちな以下のようなコードは誤りである。
/* 禁止 — クエリ条件内で var() を使っている */
:scope {
--_column-count: 3;
--_column-min-width: calc(160 / 16 * 1rem);
--_column-gap: calc(24 / 16 * 1rem);
container: --article-cards / inline-size;
}
._card {
@container --article-cards (inline-size >= calc(var(--_column-width) * var(--_column-count) + var(--_column-gap) * (var(--_column-count) - 1))) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}クエリ条件に必要な値は、最終的にリテラル値として記述する。
/* 正しい — クエリ条件はリテラル値で書く */
._card {
@container --article-cards (inline-size >= calc(160 / 16 * 1rem * 3 + 24 / 16 * 1rem * 2)) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}一方で、プロパティ値の中では var() を使ってよい。
/* 正しい — プロパティ値の中では var() を使ってよい */
._card {
--_column-count: 3;
display: grid;
grid-template-columns: repeat(var(--_column-count), minmax(0, 1fr));
}AI は calc() を使ったクエリ条件の例を示す際に、var() をそのまま条件式へ埋め込んだ誤ったコードを生成しがちである。
クエリ条件に var() が含まれていた場合は、そのまま採用せず必ず誤りとして修正すること。
確認時は以下を最低限チェックする。
@media/@containerの条件式の中にvar()が入っていないか- クエリ条件が「デバイス幅の慣習値」ではなく「レイアウト成立条件」になっているか
- gap や列数など、成立条件に必要な要素が式へ含まれているか
- 根拠のない数値を
calc()で包むだけの書き方- 例:
calc(48rem)
- 例:
- 一度しか使わない巨大な式を無コメントでその場に直接書いて可読性を落とすこと
- Intrinsic なレイアウトで解決できる問題を、無理にクエリ条件と
calc()で解決しようとすること - クエリ条件の再利用目的で
var()を使おうとすること
次のどちらかに当てはまる場合は calc() を優先する。
- 値の根拠を式として表現したほうが意図が明確になる
- 後でカラム数・個数・gap・最小幅などを変更する可能性がある
逆に、値が本当に固定であり、分解しても意味が増えない場合は単純な値をそのまま使ってよい。
ビューポート幅(またはコンテナ幅)に応じて値を滑らかに変化させる手法。
clamp(min, preferred, max) を使い、ブレイクポイントなしで最小値と最大値の間を補間する。
clamp() の preferred 値は rem(固定部分)と、文脈に応じた相対単位(100svi / 100dvi / 100lvi / 100svb / 100dvb / 100lvb / 100cqi など)の合計で構成する。
ページレベルの流動値にはビューポート単位、コンポーネントレベルの流動値には cqi 系を使う。
clamp(min-value, intercept + slope × relative-unit, max-value)
- min-value: 最小値(rem)。これ以下にはならない
- max-value: 最大値(rem)。これ以上にはならない
- slope: 変化率。基準幅 1 単位あたりの増加量
- intercept: y 切片。slope の直線が基準幅 0 のときに取る値
- relative-unit: 基準となる相対単位(
100svi、100dvi、100lvi、100svb、100dvb、100lvb、100cqiなど)
2 つの基準点を定義する:
- min-reference: 最小の基準幅(px)。ページならビューポート、コンポーネントならコンテナの最小幅。例: 400
- max-reference: 最大の基準幅(px)。ページならビューポート、コンポーネントならコンテナの最大幅。例: 1280
- min-size: min-reference での値(px)。例: 14
- max-size: max-reference での値(px)。例: 18
- base-font-size: ルートフォントサイズ(通常 16)
Step 1: slope(傾き)を求める
slope = (max-size − min-size) / (max-reference − min-reference)
例: (18 − 14) / (1280 − 400) = 4 / 880 ≈ 0.00455
Step 2: intercept(y 切片)を求める
intercept = min-size − slope × min-reference
例: 14 − 0.00455 × 400 = 14 − 1.82 = 12.18
Step 3: clamp() に組み立てる
clamp(
min-size / base-font-size * 1rem,
slope × relative-unit + intercept / base-font-size * 1rem,
max-size / base-font-size * 1rem
)例:
font-size: clamp(
14 / 16 * 1rem,
0.00455 * 100svi + 12.18 / 16 * 1rem,
18 / 16 * 1rem
);サイズトークンを単位なしの数値で持っている場合は、カスタムプロパティと calc() を組み合わせて計算をCSS側で完結させてもよい。
/* カスタムプロパティで slope / intercept を計算する例 */
--_slope: calc(
(var(--_max-size) - var(--_min-size)) /
(var(--_max-reference) - var(--_min-reference))
);
--_intercept: calc(
var(--_min-size) - var(--_slope) * var(--_min-reference)
);
font-size: clamp(
var(--_min-size) / 16 * 1rem,
var(--_slope) * 100svi + var(--_intercept) / 16 * 1rem,
var(--_max-size) / 16 * 1rem
);vwは禁止。ページレベルではsvi/dvi/lviを文脈で選ぶvhも禁止。縦方向はsvh/dvh/lvh、論理方向ではsvb/dvb/lvbを文脈で選ぶsvi/dvi/lvi/svh/dvh/lvh/svb/dvb/lvb単体をフォントサイズに使わない。ユーザーのフォントサイズ設定が無視される- preferred 値は、可読性が重要な値では必ず
rem + 相対単位の合計にし、最小値をremで固定する cqiはコンテナクエリの文脈が保証される descendant でのみ使う- ビューポート単位はメディアクエリ、
cqi系はコンテナクエリと対で考える
widthの固定値ではなくmax-inline-sizeを使うheightの固定値ではなくmin-block-sizeまたはaspect-ratioを使う- 固定値が必要な場合は
min(100%, var(--_size))でオーバーフローを防ぐ
zoomは「最後の保険」としてのみ使う。通常のレスポンシブ対応は Intrinsic なレイアウト、コンテナクエリ、メディアクエリで行い、zoomを主役にしてはいけない- 想定される最小幅が明確にあり、しかもその下ではレイアウトを組み替えても破綻を避けきれないコンポーネントに限って使う
- これはプログレッシブ・エンハンスメントである。
progress()が効かない環境でも、コンポーネントは読める・操作できる状態を保つこと - 「デザインを絶対に維持するため」に乱用してはいけない。まずは列数削減、折り返し、余白圧縮で解決を試みる
- 適用対象はコンポーネントルートに限定し、クエリコンテナが保証される文脈でのみ使う
AI 向けの判断ルール:
- 先に Intrinsic なレイアウトで解決を試す
- 次に
@containerでレイアウト変更を試す - それでも想定最小幅未満で破綻する場合に限って
zoomを追加する zoomは1を上限にし、拡大には使わない- 文字サイズそのものを viewport 単位で縮める代替として使わない
/* まずは通常のレスポンシブ対応を行う */
:scope {
container: --aticle-cards / inline-size;
}
@container --aticle-cards (inline-size < calc(200 / 16 * 1rem)) {
._card {
/* 最小幅未満では縮小を許可するが、1 を超えて拡大しない */
zoom: min(progress(100cqi, 0px, calc(200 / 16 * 1rem)), 1);
}
}このパターンは「どこに配置されるか自身は知らないコンポーネントが、想定外の狭い領域へ差し込まれたときの緊急避難」である。常用するテクニックではない。