CSS Gridがはみ出す・1frの列が縮まない原因と直し方|minmax(0,1fr)が要る場所と要らない場所をChromium実測で整理

CSS Gridがはみ出す・1frの列が縮まない原因と直し方|minmax(0,1fr)が要る場所と要らない場所をChromium実測で整理

結論:Gridが崩れる原因は「frトラックの最小値が0ではなくautoだから」の一点。minmax(0,1fr)は万能薬ではなく、効く場所と効かない場所がある

  • ① grid-template-columns:1fr 1fr の1frは「等分」ではない。frの最小値はauto(=中身のmin-content)なので、折り返せない長い文字列やpre・tableが入った列だけが勝手に太る
  • ② 2列では静かに列比だけ壊れ(実測297.92px対33.08px)、列数が増えるとページごと横スクロールになる(4列で382pxはみ出し)。「横スクロールが出ないから正常」は誤り
  • ③ 第一の手当ては minmax(0,1fr)。ただしトラックの最小値を外すだけなので、中身そのもののはみ出しは残る(4列で95px残存)
  • ④ 中身側の手当ては3つ:子に min-width:0、子に overflow:auto/hidden、子に overflow-wrap:anywhere。overflow-wrap:break-word は最小幅に影響せず効かない(実測で382pxのまま)
  • ⑤ 入れ子のグリッドは内側にも必要。外側だけ minmax(0,1fr) にすると内側の子が親トラックを越えて隣の列に6px重なる
  • ⑥ 行方向にも同じ罠がある。grid-template-rows:auto 1fr の中に高さ100%のスクローラを置くと min-height:auto で801pxはみ出す。min-height:0 が要る

結論の前に:この記事で扱う「はみ出す」は2種類ある

Gridのレイアウト崩れは、見た目が似ているだけで中身がまったく違う2種類が混在しています。最初にこれを分けないと、直したつもりで別の症状に移るだけになります。

  • A:列幅が意図どおりに分配されない。1fr 1frと書いたのに左右が3:1になる。ページ自体には横スクロールが出ないので、PCで見ている限り気づきにくい。
  • B:グリッドの中身が親の幅を超えて出る。スマホで横スクロールが出る、要素同士が重なる、右側が見切れる。

Aはトラック(列)のサイズ計算の問題、Bはアイテム(中身)のはみ出しの問題です。そして厄介なことに、どちらも原因は同じ一行——frという単位の最小値が0ではなくautoであること——から来ます。だから片方だけ直しても、もう片方が残ります。

この構造はFlexboxのmin-width:autoとまったく同じです。Flex側の挙動はflexアイテムが縮まない・ellipsisが効かない原因はmin-width:autoで詳しく書きましたが、Gridでは「行方向にも同じ罠がある」「入れ子で連鎖する」「minmax()という書き方が使える」という3点が追加で効いてきます。

なぜ1frは等分にならないのか:frの最小値はautoである

1frは「余った空間を1:1で分ける」単位だと説明されることが多いのですが、正確にはminmax(auto, 1fr)の省略形です。つまり最小値がauto=そのトラックに入っているアイテムのmin-content幅に固定されます。

min-content幅とは「これ以上狭くすると中身が確実に溢れる幅」のことで、日本語テキストなら1文字分ですが、次のようなものは一気に大きくなります。

  • スペースやハイフンを含まない長いURL・ID・トークン
  • <pre>で囲ったコードの最長行(white-space:preは折り返さない)
  • white-space:nowrapを当てたセルやボタンラベル
  • 横に長い<table>、width属性を持つ<iframe>(埋め込み地図・動画)
  • 幅が可変のflexコンテナ(これも内部に同じ問題を抱えていることが多い)

つまり1frは「余りを等分する。ただし中身の最小幅は絶対に下回らない」という指定です。中身が太いトラックは「等分の取り分」を無視して先に自分の最小幅を確保し、残りを他の列が分け合います。これが「1fr 1frなのに3:1になる」の正体です。

実測1:2列の1frは「はみ出さずに列比だけ壊れる」

ここからはChromium 141.0.0.0(headless / Playwright)で実際に測った値を出します。ビューポート幅375px、次のマークアップで計測しました。

<style>
.g { display: grid; gap: 12px; padding: 16px; grid-template-columns: 1fr 1fr; }
.c { background: #eee; }
</style>

<div class="g">
  <div class="c">https://example.com/very/long/path/name-without-break-opportunity-1234567890</div>
  <div class="c">サイドの短い文字</div>
</div>

実測結果は次のとおりです。

grid-template-columns(算出値) : 297.922px 33.0781px
左カラムの実幅                  : 297.92px
右カラムの実幅                  : 33.08px
documentElement.scrollWidth     : 375
documentElement.clientWidth     : 375
ページの横はみ出し              : 0px

注目すべきは最後の行です。ページには横スクロールが1pxも出ていません。それでも左右比は9:1近くまで崩れています。長いURLを含む列が297.92pxを先取りし、残った33.08pxを右カラムが受け取った結果です。

これが「PCでは気づかず、スマホの実機で初めて『右側が細い棒になっている』と報告が来る」典型パターンです。スマホで横スクロールが出る原因の特定手順と直し方でやるようなscrollWidth比較のチェックでは、この崩れは検出できません。横スクロールの有無はGrid崩れの判定基準にならないと覚えてください。

実測2:列数が増えるとページごとはみ出す(4列で382px)

同じ長さの文字列を4列すべてに入れ、repeat(4, 1fr)にすると症状が変わります。

grid-template-columns: repeat(4, 1fr) (長い文字列×4 / viewport 375px)

算出トラック         : 183.172px ×4
grid の scrollWidth  : 757
grid の clientWidth  : 375
ページの横はみ出し    : 382px

各トラックのmin-content幅の合計(183.172×4+gap)が親の幅を超えた瞬間、トラックの分配では吸収できなくなり、グリッドボックスごと外へ出ます。2列では「列比が壊れる」だけだったものが、4列では「382pxのページ横スクロール」になる。同じバグが、列数によって別の症状として現れるわけです。

逆に言うと、症状(列比が変/横スクロールが出る)から原因を推測するのではなく、「frトラックに折り返せない中身が入っていないか」を先に疑うのが最短ルートになります。

直し方①:minmax(0, 1fr) —— ただし万能ではない

もっとも有名な対処がminmax(0, 1fr)です。最小値を明示的に0にして、auto(=min-content)の床を外します。

/* before */
.g { grid-template-columns: 1fr 1fr; }

/* after */
.g { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* repeat を使うなら */
.g { grid-template-columns: repeat(2, minmax(0, 1fr)); }

2列のケースは、これで完全に直りました。

minmax(0,1fr) minmax(0,1fr)
算出トラック : 165.5px 165.5px   ← 完全な等分に戻る
ページの横はみ出し : 0px

ところが4列のケースでは直りきりません。

repeat(4, minmax(0,1fr))
算出トラック : 87.75px ×4        ← トラック自体は等分になった
grid の scrollWidth : 470
ページの横はみ出し : 95px        ← 95px 残る

ここが実務で一番ハマる分かれ目です。minmax(0,1fr)が直すのはトラックの幅だけで、そのトラックに収まりきらない中身がどうなるかには関与しません。87.75pxの列に折り返せない文字列を置けば、文字列はそのまま外へはみ出します。トラックは正しくなったのに、コンテンツが溢れている状態です。

minmax(0,1fr)は必要条件であって十分条件ではない——これが実測から言える結論です。中身側の手当てをもう一段入れる必要があります。

直し方②:子に min-width:0 か overflow を与える

360px幅・2列・長いURLという最初の条件で、子要素側だけをいじった場合の左カラム実幅を並べます。

親 grid-template-columns: 1fr 1fr のまま、子に指定を追加

(何もなし)          左カラム 297.92px   ← 崩れる
.c { min-width: 0 }   左カラム 174px      ← 直る
.c { overflow-x:auto }左カラム 174px      ← 直る
.c { overflow:hidden }左カラム 174px      ← 直る

min-width:0が効くのは直感どおりですが、overflowにvisible以外を指定するだけでも直るのは意外に知られていません。これは「自動最小サイズ(automatic minimum size)はoverflowがvisibleのときにだけ適用される」という仕様によるもので、Flexboxと同じルールです。

実務上ありがたいのは、横長のtableやコードブロックをoverflow-x:autoのラッパーで包むという既存の作法が、そのままGridの罠の解除にもなっている点です。実際、8セルの横長テーブルをoverflow-x:autoのdivで包んで1frの列に置いた場合を測ると、

<div class="g">(grid-template-columns: 1fr)
  <div class="wrap">(overflow-x:auto)
    <table>…8セル・white-space:nowrap…</table>

ラッパーの幅        : 375px
ラッパーのscrollWidth: 680px
ラッパー内で横スクロール可能 : true
ページの横はみ出し  : 0px

minmax(0,1fr)を書かなくても、ラッパーのoverflow-x:autoだけでページは守られています。「表とコードはoverflowラッパーで包む」を徹底していれば、Gridの罠の半分は自動的に踏まなくなるということです。

直し方③:overflow-wrap は break-word ではなく anywhere

「長い文字列なら折り返させればいい」と考えてoverflow-wrap:break-wordを当てる人は多いのですが、これはトラック幅の問題には効きません。4列・長い文字列で測った結果です。

子への指定                    算出トラック     ページの横はみ出し
--------------------------------------------------------------
(既定 normal)               183.172px ×4     382px
overflow-wrap: break-word     183.172px ×4     382px   ← 変化なし
overflow-wrap: anywhere        87.75px ×4        0px   ← 直る
word-break: break-all          87.75px ×4        0px   ← 直る

差は仕様の一行にあります。break-wordはmin-contentサイズの計算には影響しない(=レイアウト上の最小幅は長いまま)と定義されており、anywhereは影響すると定義されています。見た目の折り返しは両方とも起きるのに、トラック計算に効くのはanywhereだけ、というわけです。

注目すべきは、overflow-wrap:anywhereだけを当てた場合、minmax(0,1fr)を書かなくてもトラックが87.75pxの等分になり、はみ出しも0になった点です。min-content幅そのものが小さくなるので、frの床も一緒に下がるためです。

実務での使い分けはこうなります。

  • ユーザー投稿・URL・メールアドレス・IDが入りうる箇所 → overflow-wrap:anywhere(日本語の禁則を壊さないのでword-break:break-allより安全)
  • コード・表・埋め込み → 折り返させたくないのでoverflow-x:autoのラッパー
  • 上記の判定が事前にできない汎用レイアウト → 親にminmax(0,1fr)を保険として敷く

入れ子のグリッドは内側にも必要になる

ここが一番デバッグを長引かせるポイントです。外側のグリッドをminmax(0,1fr)に直したのに直らない、というケースの大半がこれです。

<style>
.out { display:grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap:12px; }
.in  { display:grid; grid-template-columns: 1fr; }   /* ← 内側は 1fr のまま */
</style>

<div class="out">
  <div class="in"><div class="c">長いURL…</div></div>
  <div class="c">右カラム</div>
</div>

実測値です。

外側トラック(.out)      : 165.5px 165.5px   ← 正しく等分できている
内側グリッド(.in)の幅   : 165.5px
その中の子(.c)の幅      : 183.17px          ← 親トラックより太い
子の右端                  : x=199
内側グリッドの右端        : x=182
右カラムの左端            : x=194
子と右カラムの重なり      : 6px

外側は正しく165.5pxずつに分かれています。ところが内側のグリッドが1frのままなので、その中でmin-content(183.17px)が復活し、子要素が親トラックを17px越えて、隣の列に6px重なっています。ページの横スクロールは0なので、DevToolsの「はみ出し検出」系のチェックにも引っかかりません。

frは入れ子の各階層で独立に床を持つため、はみ出しの連鎖はどこか1階層で止めないと下まで伝播します。汎用コンポーネントを書くときは、display:gridを書いたらminmax(0,1fr)をセットで書く癖にしておくのが安全です。

行方向にも同じ罠がある:min-height:auto で801pxはみ出す

Gridのfrは行にも使えるので、当然この問題も行方向に存在します。むしろ「ヘッダー固定+本文スクロール」のようなUIで踏むぶん、被害は大きくなります。

<style>
.w  { height:200px; width:300px; }
.g  { display:grid; height:100%; grid-template-rows: 40px 1fr; }
.bd { background:#eee; }            /* 中間のラッパー */
.sc { height:100%; overflow-y:auto; }  /* 実際のスクローラ */
</style>

<div class="w"><div class="g">
  <div class="hd"></div>
  <div class="bd"><div class="sc">…40行…</div></div>
</div></div>

実測結果です。

【.bd に min-height 指定なし】
ラッパー(.w)の高さ : 198px
.bd の高さ         : 960px       ← 行が中身の高さまで膨らんだ
.sc の高さ         : 960px
.sc がスクロール可能 : false      ← スクロールバーが出ない
ラッパーからのはみ出し : 801px

【.bd に min-height:0 を追加】
.bd の高さ         : 158px       ← 正しく残り高さに収まる
.sc がスクロール可能 : true
ラッパーからのはみ出し : -1px(=収まっている)

801pxはみ出し、かつスクロールバーが出ないという最悪の組み合わせです。「overflow-y:autoを書いたのにスクロールしない」という相談のかなりの割合が、Gridまたはflexの祖先にあるmin-height:autoです。

注意点として、スクローラ自身にoverflow-y:autoが付いていればその要素は自動最小サイズの対象外になるので、.bdという中間ラッパーを1枚挟んだ瞬間に発症します。実装中に「div1枚増やしたら急に崩れた」となる典型がこれです。対処は次のどちらかです。

  • 中間ラッパーにmin-height:0(またはoverflow:hidden)を与える
  • 行指定をgrid-template-rows: 40px minmax(0, 1fr)にして床を外す

なお、高さ方向の詰まりはheight:autoのアニメーションが動かない問題と原因が近い領域なので、あわせてheight:autoがアニメーションしない理由と直し方も見ておくと、高さ周りの判断が速くなります。

auto-fit / auto-fill と minmax(300px, 1fr) の落とし穴

レスポンシブなカード並びの定番、repeat(auto-fit, minmax(300px, 1fr))にも実測で確認できる罠があります。コンテナ幅を280pxにして測りました。

コンテナ幅 278px(border込み280px)
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))

算出トラック : 300px
アイテム実幅 : 300px 300px   ← コンテナ278pxより広い
→ 親からはみ出す

grid-template-columns: repeat(auto-fit, minmax(min(300px,100%), 1fr))
算出トラック : 278px
アイテム実幅 : 278px 278px   ← 収まる

minmax()の第1引数は「最小幅」なので、コンテナがその値より狭いときは素直にはみ出します。min(300px, 100%)と書けば「300pxか親幅の小さいほう」になり、狭い画面で自動的に折れます。小さい画面を持つ端末やサイドバー内に置く可能性があるコンポーネントでは、こちらを既定にしておくのが安全です。

auto-fitとauto-fillの違いも、言葉の説明より実測値のほうが早いので出しておきます。コンテナ1000px・gap20px・アイテム2個・minmax(200px,1fr)です。

auto-fit  算出トラック : 490px 490px 0px 0px   (空トラックは0に潰れる)
          アイテム実幅 : 490px

auto-fill 算出トラック : 235px 235px 235px 235px (空トラックが残る)
          アイテム実幅 : 235px

どちらもトラックは4本作られています。違いは「余ったトラックを0に潰すか(auto-fit)、幅を持ったまま残すか(auto-fill)」だけです。「アイテムが2個しかないのに左半分に寄ってしまう」ならauto-fill、「アイテムが2個のとき不自然に巨大化する」ならauto-fitが原因、と一発で切り分けられます。カード数が少ない可能性があるならauto-fit+max-widthの併用が現実解です。

%指定とgapは共存できない:33.333%×3で40pxはみ出す

Flexboxの感覚でwidth:33.333%を持ち込むと、gapと足し算になって必ずはみ出します。600pxコンテナ・gap20pxでの実測です。

grid-template-columns: repeat(3, 33.333%)
算出トラック : 199.328px ×3
clientWidth  : 598 / scrollWidth : 638
はみ出し     : 40px          ← gap 20px × 2 がそのまま溢れる

grid-template-columns: repeat(3, 1fr)
算出トラック : 186px ×3
clientWidth  : 598 / scrollWidth : 598
はみ出し     : 0px

frはgapを引いたあとの残り空間を分配するので、gapを何px入れても破綻しません。calc(33.333% - 13.333px)のような計算をしている箇所を見つけたら、それは1frで置き換えられるサインです。

横スクロールのカードUIが潰れるのは auto-columns が未指定だから

grid-auto-flow:columnで横並びカルーセルを作ろうとして「カードが潰れて全部画面内に収まってしまう」のも、同じ一族の問題です。375px幅にカード5枚で測りました。

grid-auto-flow: column                              アイテム実幅 65.39px(潰れる)
grid-auto-flow: column + overflow-x:auto            アイテム実幅 65.39px(潰れる)
grid-auto-flow: column + grid-auto-columns:minmax(0,1fr)  アイテム実幅 65.39px(潰れる)

grid-auto-flow: column + grid-auto-columns: 240px + overflow-x:auto
  アイテム実幅 240px / grid.scrollWidth 1248 / clientWidth 375 / 横スクロール可 true

grid-auto-flow: column + grid-auto-columns: 80% + overflow-x:auto
  アイテム実幅 300px / grid.scrollWidth 1548 / clientWidth 375 / 横スクロール可 true

ポイントは2つです。overflow-x:autoだけを足しても潰れたまま(暗黙トラックがautoで、親幅に収まってしまうため)。そしてgrid-auto-columnsに固定値か%を与えて初めて幅が確定し、その結果としてスクロールが必要になる。順序が逆だと永久に直りません。

さらにoverflow-x:autoを書き忘れると、実測でページ全体に1173pxの横スクロールが出ます。カルーセル実装ではgrid-auto-columnsとoverflow-xは必ずセットです。

はみ出している子要素を一発で出すコンソールスニペット

ここまでの判定を手で行うと時間がかかるので、DevToolsのConsoleに貼って「どのグリッドのどの子が何px出ているか」を一覧化するスニペットを用意しました。実際に入れ子のケースへ流して出力を確認済みです。

(() => {
  const rows = [];
  document.querySelectorAll('*').forEach(el => {
    const cs = getComputedStyle(el);
    if (cs.display !== 'grid' && cs.display !== 'inline-grid') return;
    const er = el.getBoundingClientRect();
    [...el.children].forEach(ch => {
      const cr = ch.getBoundingClientRect();
      const out = Math.round(Math.max(cr.right - er.right, er.left - cr.left));
      const inner = Math.round(ch.scrollWidth - ch.clientWidth);
      if (out > 0 || inner > 0) {
        const s = getComputedStyle(ch);
        rows.push({
          親: el.className || el.tagName,
          子: ch.className || ch.tagName,
          はみ出しpx: Math.max(out, inner),
          親のtracks: cs.gridTemplateColumns,
          子のminWidth: s.minWidth,
          子のoverflowX: s.overflowX,
          子のoverflowWrap: s.overflowWrap
        });
      }
    });
  });
  rows.length ? console.table(rows) : console.log('はみ出している子要素はありません');
  return rows;
})()

先ほどの入れ子ケースに流すと、こう出ます。

親    子   はみ出しpx  親のtracks            子のminWidth  子のoverflowX  子のoverflowWrap
out   in   17          165.5px 165.5px       auto          visible        normal
in    c    18          183.172px             auto          visible        normal

はみ出しが親から子へ連鎖している経路がそのまま行として並ぶので、どの階層にminmax(0,1fr)やmin-width:0を入れれば止まるかが即座に分かります。子のminWidthがauto、子のoverflowXがvisible、子のoverflowWrapがnormal——この3つが揃っている行が、今回の罠にかかっている行です。

なお、指定したはずのCSSがそもそも当たっていない可能性もあります。スニペットの出力と自分の書いたCSSが噛み合わないときは、CSSが効かない・上書きできない原因と直し方で詳細度・@layer・読み込み順のほうを先に潰してください。

実装チェックリスト

  • display:gridを書いたら1frではなくminmax(0, 1fr)を既定にする(repeat(3, minmax(0,1fr)))
  • 入れ子のグリッドにも同じ指定を入れる。外側だけでは止まらない
  • 行にもfrを使うならminmax(0, 1fr)、中間ラッパーにはmin-height:0
  • ユーザー入力・URLが入る箇所はoverflow-wrap:anywhere(break-wordでは効かない)
  • 表・コード・埋め込みはoverflow-x:autoのラッパーで包む。これだけでGridの罠も同時に外れる
  • カード並びはrepeat(auto-fit, minmax(min(300px, 100%), 1fr))
  • アイテム数が少なくなる可能性があるならauto-fit+max-width、常に枠を保ちたいならauto-fill
  • %指定とgapを混ぜない。1frに置き換える
  • 横スクロールUIはgrid-auto-columns(固定値か%)とoverflow-x:autoを必ずセットで
  • 画像を含む列ではaspect-ratioとobject-fitの指定も確認する(object-fitとaspect-ratioが効かない・画像の縦横比が崩れる原因と直し方)
  • 確認は必ず375px幅など狭い幅で。広い幅では症状が出ない

まとめ:疑う順番を固定する

Gridの崩れは原因が1つに収束するぶん、手順を固定してしまえば毎回同じ時間で終わります。

  1. 狭い幅で再現する(375px)。広い幅では出ない
  2. コンソールスニペットを流す。どの階層のどの子が何px出ているかを確定させる
  3. 該当階層のトラックをminmax(0,1fr)に。これで列比の崩れは止まる
  4. それでも残るはみ出しは中身側。overflow-wrap:anywhere/overflow-x:autoラッパー/min-width:0のいずれかを当てる
  5. 入れ子があれば上から順に同じ処理を繰り返す

!importantやwidth:100%を足して回る前に、この5ステップを通すほうが確実に速いはずです。

関連記事(コーディング実務Tips)

多言語・ベトナム語サイトの制作でお困りではありませんか?

フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。

多言語サイト制作の詳細を見る →

すぐに相談したい方は お問い合わせフォームへ

Web制作Tips(コーディング実務)カテゴリの最新記事