ご注意
この記事は AI のサポートを受けていますが
第9回を読み終えたのに、ネストしたレイアウトでまだ頭を抱えていませんか? Subgrid へようこそ — CSS Grid Level 2 の「スパイス」で、同じ行にあるカード同士が「高さ勝負」をやめて、きれいに揃うようになります。
この記事は、display: grid を知っているフロントエンドエンジニア向けです。カード一覧でボタンやタイトル、説明文がバラバラにズレて困っている方に向けて、Nested Grid の課題 → Subgrid の構文 → ブラウザの Track Sizing Algorithm → React + TypeScript の実例(フォールバック付き)→ よくあるハマりどころ、という流れでお話しします。
1. 課題:ネストした Grid — カードはきれいなのにボタンが「市場みたいに散らかる」
こんな経験、ありませんか?
- ケース1: 3列の商品カード一覧を Grid Layout で作る。各カードに画像、タイトル、説明、「詳細を見る」ボタン。カードAのタイトルは2行、カードBは1行 — そしてパッと、中身が全部ズレる。カードAのボタンの方がカードBより上にある。棚の横仕切りなしに物を詰め込んだみたいな見た目になります。
- ケース2: 各カードを独立した Nested Grid にして、画像 / タイトル / 説明 / ボタン用の行を分ける。でも気づくと、各カードが自分だけの行の高さを決めていて、隣のカードとは無関係。結果は? やっぱりズレる。え、なんで?
Grid Level 1 は、ネストしたトラック同期を解決できません。 各子 Grid は独立した「王国」 — 自分でトラックを決めて、隣がどれだけ高いかなんて気にしません。
Subgrid は、まさにここを埋めるために CSS Grid Level 2 で生まれました。
2. 本質:Subgrid — 親のグリッドを「借りる」、独自ルールは作らない
Subgrid は CSS Grid Layout Module Level 2 で、grid-template-columns と grid-template-rows に追加された新しい値です。
トラックを自分で宣言する代わりに、subgrid は親 Grid 上で span しているトラックを借りて、Track Sizing Algorithm(トラックサイズ決定アルゴリズム)に一緒に参加します。サイズ、gap(デフォルト)、グリッドライン名 — 全部この仕組みの中に入っています。
Subgrid は「継承」みたいなコピペじゃないですよ。
親 Grid と subgrid は同じ Track Sizing Algorithm を走らせます。subgrid の中身がレイアウト全体のトラックを引き伸ばすこともある — 親の数値をコピーして「運任せ」にするのとは全然違います。
Subgrid で何が解決できる?
| 課題 | Subgrid の役割 |
|---|---|
| カードの行がズレる | 同じ行バンドのカードが、親 Grid の行定義を共有する |
| トラックのコピペが面倒 | 直接参照するので、コードの重複が減る |
display: contents がセマンティクスを壊す |
ラッパーはGrid アイテムのまま、子 Grid が親のトラックを借りる |
| レスポンシブがつらい | 親 Grid を直せば、subgrid も自動で追従する |
3. Subgrid の構文:親からグリッドを「借りる」3ステップ
3.1. 基本構造
覚えるのは3ステップだけ — コーヒーの淹れ方より短いです:
コード例:
/* ステップ1: 親 Grid */
.parent {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, auto);
gap: 1rem;
}
/* ステップ2 & 3: 子要素を subgrid にする */
.child {
display: grid;
grid-template-rows: subgrid; /* 行だけ参照 */
/* または */
grid-template-columns: subgrid; /* 列だけ参照 */
/* または */
grid-template: subgrid / subgrid; /* 両方参照 */
}
3.2. 行だけ? 列だけ? 両方?
| 使い方 | CSS | 意味 |
|---|---|---|
| 行だけ | grid-template-rows: subgrid; |
親の行を借りる、列は自分で管理 |
| 列だけ | grid-template-columns: subgrid; |
親の列を借りる、行は自分で管理 |
| 両方 | grid-template: subgrid / subgrid; |
行も列も借りる |
3.3. 借りられるもの・借りられないもの
要素が subgrid になると:
- ✅ span しているトラックシステム(
1fr、minmax()、auto、px、%など) - ✅ 親 Grid の間隔(gap) — デフォルトで継承、独自の
gapで上書きも可能 - ✅ グリッドライン名(named lines)
- ❌ Grid area(エリア名は参照できない)
/* named lines 付きの親 Grid */
.parent {
display: grid;
grid-template-columns: [main-start] 1fr [content-start] 2fr [content-end] 1fr [main-end];
grid-template-rows: [header] auto [body] auto [footer] auto;
}
/* subgrid はこれらのライン名を引き継ぐ */
.child {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
/* 孫要素は親のライン名を使える */
.grandchild {
grid-column: content-start / content-end;
grid-row: body / footer;
}
Subgrid の真価は行揃えだけではありません。
大規模なデザインシステムでは、親 Grid で定義した Named Lines([content-start] / [content-end] など)を subgrid 経由で共有できることが、最大のメリットになる場合があります。孫要素が grid-column: content-start / content-end のように、ルート Grid と同じライン名をそのまま参照できる — トラックサイズの同期に加えて、レイアウトの語彙をツリー全体で共有できるのが Subgrid の強みです。
4. Subgrid vs 普通の Nested Grid:何が違うの?
4.1. ざっくりイメージ
4.2. 詳細比較表
| 観点 | 普通のネスト Grid | Subgrid |
|---|---|---|
| Track sizing | 自分で定義 | 親から借りる |
| 子 Grid の同期 | ❌ できない | ✅ できる(同じトラックバンド内で Track Sizing を共有) |
| Named lines | 借りられない | 借りられる |
gap |
自分で設定 | デフォルトは親のもの(上書き可) |
| トラック数 | 自分で決める | 親上の span 数と同じ |
| レスポンシブ | 各子 Grid を個別に修正 | 親 Grid を直せばOK |
| コードの複雑さ | 高く、重複しがち | 低い(DRY) |
4.3. 同じ HTML、違う CSS — 見た目が全然変わる
<!-- 同じ HTML -->
<div class="grid">
<div class="card">
<h2>長いタイトル</h2>
<p>短い説明</p>
<button>見る</button>
</div>
<div class="card">
<h2>タイトル</h2>
<p>とてもとても長い説明で、何行にもわたるかもしれない</p>
<button>見る</button>
</div>
</div>
普通のネスト Grid: 各カードが行の高さを自分で決める → 「見る」ボタンが上下にバラバラ。
Subgrid: 同じトラックバンドに属するカード同士が、親 Grid のトラックサイズを共有する → ボタンがきれいに揃って、ずっと「整った」見た目になります。
上の2枚はプレースホルダーです。Qiita で読まれる記事には、実際のスクリーンショットが効きます。 特に Chrome DevTools → Grid のオーバーレイで親トラックと subgrid の span 領域を見せる1枚は、Mermaid 図5枚分くらいの説得力があります。デモを動かしたら、Before/After の比較画像に差し替えてください。
5. ブラウザは Subgrid をどう処理するの?(アルゴリズムの視点)
5.1. 内部の処理フロー
subgrid に出会っても、ブラウザは display: grid を無視しません — 要素はGrid コンテナのまま、ただ独自のトラックを定義せず、親 Grid 上で span したトラックを借ります。CSS Grid Level 2 の仕様に沿って、処理の流れはこんな感じです:
覚えておいてほしいのは、subgrid は新しいトラックを生み出さない — 親にあるトラックを借りるだけ、ということです。
デバッグのコツ: Chrome DevTools → Grid のオーバーレイをオンにして、親のトラックと subgrid の span 領域を確認してみてください。subgrid の不具合の多くは、トラック不足か span の数が合っていないことから来ます — オーバーレイを見ればすぐわかりますよ。
5.2. Subgrid はトラックの「コピー」じゃないです
多くの人(自分も最初はそうでした)が、grid-template-rows: subgrid を「親の行をそのままコピーする」って思いがち。これは大きな勘違いです。
ブラウザはトラックの値をコピーしません。subgrid は親 Grid のサイズ計算ループ(Grid Level 2 仕様の track sizing)に直接参加します。subgrid の中身が親のトラックを引き伸ばすこともある — 決まった数値を受け取るだけ、ではないんです。
イメージしやすい例: subgrid の行1に巨大な画像がある → 親 Grid の行1も広がって収める必要がある。両方が同じ計算ループにいるからで、別世界じゃないんです。
6. 実践例:React + TypeScript で作る商品グリッド
さあ、商品一覧コンポーネントを作りましょう — ゴールは、同じ行のカードで画像、タイトル、説明、価格、ボタンが棚に並べたみたいに揃うことです。
このデモは Subgrid の仕組みを説明するために簡略化しています。
本番の Grid Layout では、親 Grid にカード内部の行を grid-template-rows でハードコードするパターンはあまり見ません。一般的には grid-auto-rows や repeat(auto-fit, minmax(...)) と組み合わせ、カード側で grid-template-rows: subgrid + grid-row: span N を使います。以下のコードはその本番に近い形です。
6.1. 要件
- レスポンシブグリッド:デスクトップ3列、タブレット2列、モバイル1列
- 各カード:画像、タイトル、説明、価格、「カートに追加」ボタン
- 同じトラックバンドのカード同士が揃うこと(デスクトップ3列ならカード1〜3など)
- 古いブラウザ向けのフォールバックあり(ただし、フォールバックでカード間の行同期は期待しないでくださいね)
6.2. React + TypeScript コンポーネント
import React from 'react';
import './ProductGrid.css';
interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
}
interface ProductGridProps {
products: Product[];
}
export const ProductGrid: React.FC<ProductGridProps> = ({ products }) => {
return (
<div className="product-grid">
{products.map((product) => (
<article key={product.id} className="product-card">
<div className="product-card__image">
<img src={product.imageUrl} alt={product.name} loading="lazy" />
</div>
<h3 className="product-card__name">{product.name}</h3>
<p className="product-card__description">{product.description}</p>
<div className="product-card__price">{product.price.toLocaleString()}円</div>
<button className="product-card__button">カートに追加</button>
</article>
))}
</div>
);
};
6.3. Subgrid 付き CSS
/* ========================================
親 Grid — 列を定義、行は implicit で生成
======================================== */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: auto; /* カードの span 数に応じて行が自動生成される */
gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
/* ========================================
各カードが subgrid になる
======================================== */
.product-card {
display: grid;
/* フォールバック: 各カードが独自に行を管理(カード間は同期しない) */
grid-template-rows: auto auto auto auto auto;
background: #ffffff;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
/* ========================================
カード内の各要素
各要素を特定の行に配置し、
その行はカード間で同期される
======================================== */
.product-card__image {
grid-row: 1; /* 行1: 画像 */
overflow: hidden;
border-radius: 8px;
}
.product-card__image img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.product-card__name {
grid-row: 2; /* 行2: タイトル */
font-size: 1.125rem;
font-weight: 600;
margin: 0;
overflow-wrap: break-word;
}
.product-card__description {
grid-row: 3; /* 行3: 説明 */
font-size: 0.875rem;
color: #666;
margin: 0;
overflow-wrap: break-word;
/* 表示行数を制限 */
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-card__price {
grid-row: 4; /* 行4: 価格 */
font-size: 1.25rem;
font-weight: 700;
color: #2563eb;
margin: 0;
}
.product-card__button {
grid-row: 5; /* 行5: ボタン */
background: #2563eb;
color: #ffffff;
border: none;
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
/* ボタンの高さは行5に揃うので
カード間で自動的に同期される */
}
.product-card__button:hover {
background: #1d4ed8;
}
/* ========================================
レスポンシブ
======================================== */
/* タブレット: 2列 */
@media (max-width: 900px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* モバイル: 1列 */
@media (max-width: 600px) {
.product-grid {
grid-template-columns: 1fr;
}
}
/* ========================================
Subgrid — 対応ブラウザのみ適用
======================================== */
@supports (grid-template-rows: subgrid) {
.product-card {
grid-template-rows: subgrid;
grid-row: span 5; /* カードが5行分 span(行1〜行5) */
}
}
6.4. この例で Subgrid はどう動く?
まとめると: 親 Grid にトラック(grid-auto-rows による implicit でも explicit でも)が必要で、subgrid が借りられます。各カードは span 5 行、子要素に正しい grid-row を指定。同じトラックバンドのカード(3列グリッドの1行目ならカード1〜3)は、Track Sizing Algorithm により各行の高さが揃います。
6.5. 見落としがちな罠:「行バンド」単位の同期
subgrid が全カードを一括で同期するわけじゃない、って覚えておいてください。同期されるのは、親 Grid 上で同じトラックセットを span しているカードだけです。
3列レイアウトで各カードが grid-row: span 5 の場合:
| カード | 親 Grid 上の位置 | 同期される相手 |
|---|---|---|
| カード1〜3 | 行1〜5(列1、2、3) | ✅ 互いに |
| カード4〜6 | 行6〜10(implicit、列1、2、3) | ✅ 互いに |
| カード1 vs カード4 | 行バンドが違う | ❌ バンド間は同期しない |
仕様どおりで、バグじゃないんです。各バンドに独自のトラックがあります。複数行のカードグリッドでも、このパターンで問題ないことが多いです — ユーザーは普通、同じ行に並んでいるカード同士を比較するので、1行目のボタンと3行目のボタンが揃う必要はあまりないですよね。
explicit tracks を使う場合:
デモを最小構成にしたいときは、親に行を明示的に書くこともできます:
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto auto auto auto auto; /* 5行を明示 */
}
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5;
}
ただし本番では、上記の grid-auto-rows パターンの方が一般的です。explicit と implicit はレイアウトに合わせて柔軟に組み合わせてください。
7. よくあるハマりどころ(と、あまり知られてないコツ)
7.1. 罠 #1:親のトラック定義が subgrid の span より不足している
カードに grid-template-rows: subgrid を書いたのに、親 Grid 側のトラック定義が subgrid が参照するトラック数より不足している — 期待した同期レイアウトにならないことがあります。 explicit トラックも implicit トラックも生成されますが、span 数と噛み合わないと Track Sizing Algorithm が意図どおりに動きません。
/* ❌ NG: 親に行定義がなく、span よりトラックが不足する可能性 */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* grid-template-rows も grid-auto-rows もない */
}
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5; /* ❌ 参照すべき5行分のトラックが不十分 */
}
結果: Subgrid が参照するトラック数より親側のトラック定義が不足している場合、レイアウトは「動く」けど、期待した同期にはならない。implicit トラックが生まれることもありますが、span 数との整合を確認してください。
対処法: 親 Grid に十分なトラックを用意する — explicit でも implicit でもOKです。
/* ✅ 方法1: Explicit tracks */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto auto auto auto auto;
}
/* ✅ 方法2: grid-auto-rows で implicit tracks */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: auto;
}
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5; /* ✅ implicit で5行分確保 */
}
7.2. 罠 #2:grid-template-areas は subgrid で借りられない
親 Grid に grid-template-areas を宣言して、subgrid で 'header' や 'sidebar' を呼び出そうとしても… 残念ながら subgrid はエリア名を参照できません。
/* ❌ grid-template-areas は参照できない */
.parent {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
}
.child {
display: grid;
grid-template-rows: subgrid;
/* ❌ 'header'、'sidebar' などは使えない */
}
対処法: subgrid で参照したい場合は、named lines を使いましょう。
7.3. 罠 #3:span が子要素の数と合わない
子要素が5つなのに grid-row: span 3 だけ — スペースが足りなくて、要素が同じ行に詰まったり、はみ出したりします。
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* ❌ 3行しかないのに子要素は5つ */
}
対処法: grid-row: span N の N を、必要な行数と一致させてください。
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5; /* ✅ 子要素5つに対応する5行 */
}
7.4. 罠 #4:Intrinsic sizing がまだ「暴れる」
subgrid は万能薬じゃありません。コンテンツが長すぎるとカードが膨らむことも — そのときは子要素に min-width: 0 や overflow: hidden を、必要なら subgrid 自体にも指定してみてください。
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5;
/* コンテンツが長すぎて縮めたい場合 */
min-width: 0; /* 必要になることがある */
}
または各子要素で個別に対処:
.product-card__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
7.5. あまり知られてないコツ:多段の subgrid
subgrid は何段でもネストできます。2段目の subgrid は直接の親(1段目の subgrid かも)からトラックを借ります。1段目がルート Grid から借りているので、ツリー全体が同じ track sizing ループに参加します。
ネストしたコンポーネントでもレイアウトを貫通して同期したいときに便利です — display: contents のハックは不要になります。
8. ブラウザサポートとフォールバック
8.1. 2026年の状況 — 本番で使えます
| ブラウザ | 対応バージョン | 備考 |
|---|---|---|
| Chrome | 117+(2023年9月) | フルサポート |
| Edge | 117+(2023年9月) | フルサポート |
| Firefox | 71+(2019年12月) | かなり早くから対応 |
| Safari | 16.0+(2022年9月) | フルサポート |
| IE11 | ❌ 非対応 | polyfill 不可 |
まとめ: Chrome、Edge、Firefox、Safari はすべて対応済みです。2026年なら Subgrid は本番で気軽に使えます — IE11 をまだ背負っている場合は… 頑張ってください、polyfill はないですよ。
8.2. @supports によるフォールバック(プログレッシブエンハンスメント)
シンプルなやり方:基本レイアウトをデフォルトにして、対応ブラウザだけ Subgrid を有効化。ProductGrid.css の 6.3 と同じパターンです。
フォールバックの grid-template-rows: auto auto auto auto auto は各カード内部のレイアウトだけを維持します — Subgrid のようなカード間の行同期はできません。古いブラウザ対応では、このトレードオフを受け入れてくださいね。
/* デフォルト: 全ブラウザ向けフォールバック */
.product-card {
display: grid;
grid-template-rows: auto auto auto auto auto;
}
/* 対応ブラウザのみ Subgrid を適用 */
@supports (grid-template-rows: subgrid) {
.product-card {
grid-template-rows: subgrid;
grid-row: span 5;
}
}
列向けにも使えます:
@supports (grid-template-columns: subgrid) {
/* 列用の Subgrid コード */
}
9. チェックリスト:Subgrid が「動かない」? 諦める前にここを確認
subgrid が黙っている、レイアウトがズレる? すぐ Flexbox に逃げないで — このチェックリストを一通り見てみてください:
-
複数行のカードを横断して同期を期待していない?
Subgrid は同じ行バンドのカードだけ同期します。2行目のカードと1行目のカードのボタンが揃わないのは仕様どおりで、バグじゃないです。 -
親 Grid のトラック定義は subgrid の span と整合している?
explicit/implicit トラックが subgrid の参照数より不足していると、期待した同期レイアウトにならないことがあります。 -
subgrid 要素に
display: gridはある?
これがないと Grid コンテキストに入れません。 -
spanは十分?
子要素5つ →grid-row: span 5が必要(列ならgrid-column: span N)。 -
grid-template-areasを使おうとしていない?
Subgrid はエリア名を借りられない — named lines に切り替えましょう。 -
gapが意図せず上書きされていない?
デフォルトは親のgapですが、subgrid 側のgapで上書きできます。 -
ブラウザは対応している?
@supports (grid-template-rows: subgrid)で確認してみてください。 -
要素は親の直接の Grid アイテム?
直接の Grid アイテムだけが subgrid になれます。 -
Intrinsic sizing でまだ膨らむ?
min-width: 0は試した?
Subgrid だけではmin-width: autoの問題は全部解決しません。
10. まとめ
Subgrid はレイアウト好きの「あったらいいな」程度の機能じゃなくて、CSS Grid Level 2 が Nested Grid のトラック同期に欠けていたピースを埋めたものです。
Subgrid は Flexbox の代替ではありません
レイアウト手法の使い分けを整理しておきましょう:
| 用途 | おすすめ |
|---|---|
| 1次元レイアウト(横並び・縦並び) | → Flexbox |
| 2次元レイアウト(行と列を同時に制御) | → Grid |
| ネストしたトラック同期(カード間の行揃えなど) | → Subgrid |
Subgrid は Grid Layout の延長線上にあり、Flexbox と競合するものではありません。
仕事で使うなら、この6点を覚えればOK:
- Subgrid は親 Grid 上で span したトラックを借りる — コピーじゃなく、親と同じ track sizing に参加する。
- 行、列、または両方を借りられる。
- 同じトラックバンドの subgrid はサイズを共有 → 隣のカード同士が揃う。
- デフォルトで親の
gapを使う(上書き可);named lines は借りられる、grid-template-areasは借りられない。 - モダンブラウザは対応済み — 2026年なら本番で安心。
- Intrinsic sizing(
min-width: autoなど)は、まだ手動で対処が必要。
いつ使う? いつ使わない?
| 状況 | おすすめ |
|---|---|
| カードグリッドで、カード内の要素を揃えたい | ✅ 行の subgrid |
| 複雑な表で header/footer を同期したい | ✅ 行 + 列の subgrid |
| ネストしたコンポーネントで同じグリッド系を使いたい | ✅ 多段 subgrid |
| シンプルなレイアウトで同期不要 | ❌ 普通の Grid で十分 |
| まだ IE11 をサポートしている | ❌ 他に道はない |
参考資料
- MDN: Subgrid – CSS Grid Layout
- WebKit Blog: Subgrid – how to line up elements
- CSS Grid Layout Module Level 2 Specification
- Can I Use: CSS Subgrid
- Smashing Magazine: CSS Grid Level 2 – Here Comes Subgrid
次回
【Frontend CSS – パート11】ブラウザから見たIntrinsic Sizing ─ コンテンツとコンテナはどちらがサイズを決めるのか?

