Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Last active March 30, 2026 19:03
Show Gist options
  • Select an option

  • Save tak-dcxi/acd985cd8b7486a807f1c240731cc7e0 to your computer and use it in GitHub Desktop.

Select an option

Save tak-dcxi/acd985cd8b7486a807f1c240731cc7e0 to your computer and use it in GitHub Desktop.
CSS Responsive Rules

CSS Responsive Rules

目的

レスポンシブ実装の判断基準を明確にし、不要なブレイクポイントとコード量を最小化する。 CSS はブラウザへの「提案」であり、厳密な命令ではない。ブラウザに委ねられる仕事は委ねる。

基本方針: エスカレーション順序

レスポンシブ対応は以下の優先順序で検討する。上位で解決できるなら下位は使わない。 すべての要素がレスポンシブである必要はない。最初に「そもそもレスポンシブにすべきか」を判断する。

0. 静的(Static)

本質的に固定サイズの要素はレスポンシブにしない。

  • タグ・バッジ・ラベルなど、内容量が少なく固定的な小さな要素
  • アイコン(1em でフォントサイズに追従するだけで十分)
  • 装飾的なボーダーや区切り線
  • 固定サイズのアバターやロゴマーク

これらに clamp() やクエリを適用するのは過剰であり、コードの複雑さだけが増す。

1. 固有のレスポンシブ性(Intrinsic)

クエリを一切使わず、要素自体が自然に適応する手法を最優先にする。

  • 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 で定義されている、柔軟でコンポーネントに依存しないレイアウトシステムの利用を優先する。

2. コンテナサイズクエリ(Container Queries)

親要素のサイズに応じた切り替えが必要な場合に使用する。

使うべきケース:

  • コンポーネントがサイドバー・モーダル・カード内など複数のコンテキストに配置される
  • ビューポートではなく、配置先の幅に応じてレイアウトを変える必要がある

container-name は必須とする

コンテナがネストされたとき、@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 の必須化

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-nameanimation-timeline など)
  • 「そのプロパティでは -- が必須か?」を個別に覚える必要が減る
  • 将来 CSS 標準側のキーワードが増えても、衝突リスクを下げられる
  • 自分が定義した名前だと即座に判別できる

container の適用箇所

  • コンポーネントルート、またはコンポーネント内で本当に必要な箇所のみに適用する
  • container-type は後述する副作用が存在するため、「とりあえず全体に container-type を付ける」ことはしない
  • .container のようなユーティリティクラスは作ってはいけない

.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-grow flex-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 のようなクラス名を必須とする

3. メディアクエリ(Media Queries)

ビューポート全体のレイアウト切り替えなど、上位の手法では対応できない場合にのみ使用する。

使うべきケース:

  • ページ全体のマクロレイアウト(2カラム ↔ 1カラム)の切り替え
  • ビューポートに固定された要素(モーダル、トースト、ポップオーバーされたコンテンツ)
  • そのブロックがページに対して常に横幅100%であることが保証され、他のコンテキストに配置されることが無いと言い切れる場合(ヒーローヘッダーなど)

ビューポート単位とコンテナ単位の原則

vw は使用禁止

vw は使用してはいけない。接頭辞なしのビューポート単位は large viewport ベースに倒れるため、意図が曖昧で安全ではない。横方向は論理的指定の svi / dvi / lvi を文脈に応じて選ぶ。

  • svi: ブラウザUIが表示された最小インラインサイズでも安全に収めたい場合
  • dvi: ブラウザUIの出入りに追従して、現在見えている領域へ合わせたい場合
  • lvi: ブラウザUIが引っ込んだ最大インラインサイズを基準にしたい場合

原則的には svi を優先する。後述する cqi は基準コンテナが見つからない場合は svi を返すという仕様が存在し、それと合わせる。

vh も使用禁止

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 系はコンテナクエリと対で考える

cqi / cqb / cqw / cqmin / cqmax は、名前付きのクエリコンテナが存在することを前提に使う。コンポーネントに雑に cqi を書いてはいけない。

  • cqi は descendant 側で使う。コンテナ自身の自己サイジングに使わない
  • cqi を使う場合は、同じコンポーネント内に container: --name / inline-size@container --name (...) が存在するかを確認する
  • 「その要素に有効なクエリコンテナが必ずある」と言い切れない場所では cqi を使わない
  • cqb / cqw / cqmin / cqmaxcontainer-type: size を適用しないと有効化されず、container-type: size は固定の高さを持つ必要があるため有効に利用できる場面はほぼ存在しない。原則的に使わない方針とする

有効な query container が無い場合、container query length units は small viewport 単位へフォールバックする。そのため、コンポーネントのベーススタイルに無造作に cqi を書くと、配置先によって意味が変わってしまう

sv* / dv* / lv* の実装上の注意

  • svi / dvi / lvi / svb / dvb / lvb を直接使うと Chrome 系ブラウザのズーム時に拡大されずアクセシビリティに影響を及ぼす可能性がある
  • 原則的には sv* / dv* / lv* は使わざるを得ないケースのみ使用し、% や Intrinsic なレイアウトで解決できる場合にはそれを優先する
  • プロジェクト全体でビューポート単位ベースの流動値を多用する場合は、都度生値を書くのではなく、トークンまたはユーティリティへ集約する
  • Chrome 145 から条件下で vw 系がスクロールバーを含まずに計算される挙動に変わった
    • 具体的には :rootscrollbar-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)) { ... }

コンテナクエリでは inline-size を使う

コンテナクエリは論理的な方向指定が前提のため、width ではなく inline-size を使用する。

/* 正しい — 論理プロパティ */
@container --card (inline-size >= calc(420 / 16 * 1rem)) { ... }

/* 禁止 — 物理プロパティ */
@container --card (width >= calc(420 / 16 * 1rem)) { ... }

レイヤー別の推奨手法

カスケードレイヤーごとに、優先すべきレスポンシブ手法が異なる。

@layer pages(ページレイアウト)

メディアクエリを優先する。ページ全体のマクロレイアウトはビューポートに依存するため。

@layer components(コンポーネント)

コンテナクエリを優先する。コンポーネントはどのコンテキストに配置されるか予測できないため、ビューポートではなく親要素のサイズに基づいて適応すべき。 例外として、モーダルやトーストなど、ビューポートに依存するコンポーネントはメディアクエリを使用する。


ブレイクポイントの考え方

デバイスカテゴリに基づくブレイクポイントを設けない

2000 以上のデバイスサイズが存在する現在、sp / tablet / pc のようなカテゴリ分けは機能しない。 原則として sp / tablet / pc のようなデバイスを連想される命名は禁止とする。

コンテンツに基づいて決定する

レイアウトが崩れるポイントでブレイクポイントを設定する。 .sp-only / .pc-only のような表示切り替えクラスは使用してはいけない。

ブレイクポイントの単位

メディアクエリのブレイクポイントには rem を使用する。 フォントを拡大したユーザーは実質的に利用可能なスペースが減るため、rem にすることで適切にフォールバックする。 後述する calc() テクニックを使い、rem を使う際は @media (width >= calc(640 / 16 * 1rem)) のようにピクセル値を変換するように記述すること。

calc() を使用してマジックナンバーを回避する

メディアクエリやコンテナクエリの条件、ならびにサイズ計算には calc() を積極的に使用する。 calc() は単なる四則演算のための関数ではなく、レイアウトの成立条件をコード上に明示し、マジックナンバーを減らすための手段である。

固定の数値をそのまま書くと、その値が何を根拠に決まっているのかが後から分からなくなる。 calc() を使い、以下のような意味のある値の組み合わせとして表現する。

  • カラム最小幅 × カラム数
  • アイテム幅 × 個数 + gap × 隙間数
  • コンテンツ幅 + padding + border
  • 基準サイズ同士の差分から導かれる流動値

これにより、48rem960px のような閾値を「たまたま決めた数値」ではなく、「そのレイアウトが成立するために必要な幅」として表現できる。

基本原則

クエリの閾値は、デバイスカテゴリではなくコンテンツに基づいて決定する。 先に 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 出力に対する注意

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: 基準となる相対単位(100svi100dvi100lvi100svb100dvb100lvb100cqi など)

計算式

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

  • zoom は「最後の保険」としてのみ使う。通常のレスポンシブ対応は Intrinsic なレイアウト、コンテナクエリ、メディアクエリで行い、zoom を主役にしてはいけない
  • 想定される最小幅が明確にあり、しかもその下ではレイアウトを組み替えても破綻を避けきれないコンポーネントに限って使う
  • これはプログレッシブ・エンハンスメントである。progress() が効かない環境でも、コンポーネントは読める・操作できる状態を保つこと
  • 「デザインを絶対に維持するため」に乱用してはいけない。まずは列数削減、折り返し、余白圧縮で解決を試みる
  • 適用対象はコンポーネントルートに限定し、クエリコンテナが保証される文脈でのみ使う

AI 向けの判断ルール:

  • 先に Intrinsic なレイアウトで解決を試す
  • 次に @container でレイアウト変更を試す
  • それでも想定最小幅未満で破綻する場合に限って zoom を追加する
  • zoom1 を上限にし、拡大には使わない
  • 文字サイズそのものを 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);
  }
}

このパターンは「どこに配置されるか自身は知らないコンポーネントが、想定外の狭い領域へ差し込まれたときの緊急避難」である。常用するテクニックではない。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment