#React#TypeScript

CSS Grid 精通:从二维布局到实战

Flexbox 是一维的,Grid 是二维的——真正的页面布局该用 Grid。本文讲清 Grid 的核心概念(行/列/区域/轨道)、fr 单位与 minmax、grid-template-areas 语义化布局、响应式 Grid(auto-fit/auto-fill)、Grid vs Flexbox 的取舍,带你从"能用 Grid"到"精通 Grid"。

本文要点

  • 主题聚焦:React、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月24日·更新于 2026年7月23日

CSS Grid 精通:从二维布局到实战

"Flexbox 排一行,Grid 排一张表。"

很多开发者写了好几年 React,布局还是清一色的 Flexbox:display: flex 一把梭,外层 flex、内层 flex、嵌套 flex 套娃。能跑吗?能。优雅吗?不。

页面骨架这种典型的"二维布局"——同一时刻你既要控制行又要控制列——用 Flexbox 写出来基本就是一堆嵌套 + 一堆 flex-grow + 一堆 media query 凑出来的"补丁墙"。

CSS Grid 是 2017 年正式落地的布局标准,它是浏览器里第一个真正为二维布局设计的系统。一旦你建立起了 Grid 的心智模型,90% 的"响应式布局难题"会直接消失。

这篇文章不讲 API 罗列,只讲心智模型 + 实战取舍。读完你应该能:

  • 看着设计稿,脑子里直接浮出 grid-template-*
  • 知道什么时候用 Grid,什么时候用 Flex
  • 写出零 media query 的自适应卡片网格
  • 用 grid-template-areas 写出"看一眼 CSS 就懂页面结构"的语义化布局

一、为什么是 Grid:二维布局的本质

理解 Grid 的第一步,是放下 Flexbox 的"一维"直觉。

Flexbox 是一维的:一次只能沿一个方向(主轴)排列。就算你 flex-wrap: wrap 让它换行,你也无法控制第二行有几个、每行怎么对齐——那是浏览器自己决定的。

Grid 是二维的:你可以同时显式控制行和列。每个子项的位置都可以用"第几行第几列"精确描述,行与列之间是平等的、对称的。

看这张对比图就懂了:

text
Flexbox(一维)              Grid(二维)
┌──────────────────────┐    ┌─────┬─────┬─────┐
│ [A][B][C]            │    │  A  │  B  │  C  │
┌──────────────────────┐    ├─────┴──┬──┴─────┤
│ [D][E]  ← 换行了,   │    │   D    │   E    │
│         但你控制不了 │    │        │        │
└──────────────────────┘    ├────────┴────────┤
                            │       F         │
                            └─────────────────┘
       一切靠主轴                  行列同时控制

什么时候该用 Grid?一句话:只要你的布局在视觉上呈现为"表格状"或"卡片状"——也就是同一行有多个对齐的列,且跨行也有对齐关系——就用 Grid。

具体场景:

  • 整页骨架(header / sidebar / main / footer)
  • 仪表盘卡片墙
  • 图库 / 商品列表
  • 表单的双列布局(label | input)
  • 任何"行+列都要对齐"的 UI

反过来,组件内部一行水平排列的按钮、一行图标 + 文字——这些是 Flex 的主场,别强行用 Grid。


二、核心属性:grid-template 与轨道(Track)

Grid 的核心就一组属性:在容器上定义网格结构,子项自动落入格子。

2.1 三个最常用的容器属性

css
.grid {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;  /* 三列:固定 + 两份弹性 */
  grid-template-rows: auto 1fr auto;     /* 三行:内容高 + 弹性 + 内容高 */
  gap: 16px;                              /* 行列间距,简洁版是 row-gap/column-gap */
}

这里出现了一个核心概念:轨道(Track)。一条轨道就是网格里"一整列"或"一整行"。grid-template-columns 定义的就是列轨道的宽度序列。

布局示意:

text
grid-template-columns: 200px 1fr 1fr;
grid-template-rows:    auto  1fr  auto;

     col1(200px)  col2(1fr)   col3(1fr)
   ┌───────────┬───────────┬───────────┐
r1 │           │           │           │  auto(内容撑开)
   ├───────────┼───────────┼───────────┤
r2 │           │           │           │  1fr(吃掉剩余空间)
   │           │           │           │
   ├───────────┼───────────┼───────────┤
r3 │           │           │           │  auto
   └───────────┴───────────┴───────────┘
              gap = 16px

2.2 fr 单位:Grid 的灵魂

fr(fraction,分数)是 Grid 引入的弹性单位。它表示把剩余空间按份数分配。

css
/* 三列等宽 */
grid-template-columns: 1fr 1fr 1fr;

/* 侧边栏 + 主区,2:5 的比例 */
grid-template-columns: 240px 5fr;     /* 240px 固定,剩下全给主区 */

/* 经典的 1:2:1 */
grid-template-columns: 1fr 2fr 1fr;

关键认知:fr 分配的是剩余空间,不是总空间。如果某列内容撑开了,固定列优先,fr 列分剩下的。这和 Flexbox 的 flex-grow 一脉相承,但 Grid 里写起来更直观。

2.3 minmax:Grid 真正的弹性之美

光有 fr 还不够——1fr 在内容很少时会塌成 0,在内容很长时会被撑爆。minmax() 解决这个问题:

css
/* 这一列最小 200px,最大 1fr */
grid-template-columns: minmax(200px, 1fr) 3fr;

minmax(min, max) 让轨道有"弹性区间":不会小于 min,不会大于 max。这是后面响应式 Grid 的基石。

2.4 repeat():少写点废话

css
/* 不写 */
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;

/* 写 */
grid-template-columns: repeat(6, 1fr);

/* 混合写 */
grid-template-columns: 200px repeat(3, 1fr) 100px;

repeat(n, size) 就是语法糖,但配合下一节的 auto-fit 才是它的真正威力。


三、grid-template-areas:语义化命名布局

这是我个人认为 Grid 最被低估的特性。它让你的 CSS 读起来就像一张 ASCII 画的布局图。

3.1 用名字摆出整个页面

css
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 60px 1fr 50px;
  grid-template-areas:
    "header  header"
    "sidebar main"
    "footer  footer";
  min-height: 100vh;
  gap: 12px;
}

.layout > header  { grid-area: header; }
.layout > aside   { grid-area: sidebar; }
.layout > main    { grid-area: main; }
.layout > footer  { grid-area: footer; }

看 grid-template-areas——它就是页面的形状。你不用看 HTML,光看 CSS 就知道这个页面长什么样:

text
┌─────────────────────────────┐
│           header            │
├──────────┬──────────────────┤
│          │                  │
│ sidebar  │       main       │
│          │                  │
├──────────┴──────────────────┤
│           footer            │
└─────────────────────────────┘

子项只要写一句 grid-area: xxx 就归位了。不用算行号、列号,不用 span,不用 grid-column: 1 / 3 这种"算术题"。

3.2 几条规则记住就行

  1. 每个名字必须形成一个矩形区域(不能是 L 形、T 形)。
  2. 要让某块跨多列/多行,就把名字重复写多次(上面 header 跨两列就是这么写的)。
  3. 用 . 表示"这个格子空着"。
  4. 每一行的格子数必须相等。

3.3 配合 media query 重排布局

这是 grid-template-areas 最爽的用法——响应式重排只需要改一片字符串,HTML 完全不动:

css
/* PC:左侧栏 */
@media (min-width: 768px) {
  .layout {
    grid-template-columns: 220px 1fr;
    grid-template-areas:
      "header  header"
      "sidebar main"
      "footer  footer";
  }
}

/* 移动端:侧栏沉到底部 */
@media (max-width: 767px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "sidebar"
      "footer";
  }
}

JSX 里就一句:

tsx
<div className="layout">
  <header />
  <aside />
  <main />
  <footer />
</div>

这是我做项目时最常推荐给团队的 Grid 用法。它的可读性吊打任何行号计算。


四、响应式 Grid:auto-fit / auto-fill + minmax

接下来这一节,是 Grid 最"魔法"的部分——写一行 CSS,做出无 media query 的自适应卡片墙。

4.1 自适应卡片网格:一行 CSS

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

这一行的效果:

  • 视口宽 → 自动放下更多卡片,每张至少 240px
  • 视口窄 → 自动减少列数,每张仍保持 240px 以上
  • 完全不需要 media query

示意(视口从宽到窄):

text
视口 1200px:每张 240px+,放 4 列
┌────┬────┬────┬────┐
│ C1 │ C2 │ C3 │ C4 │
└────┴────┴────┴────┘

视口 800px:放 3 列
┌─────┬─────┬─────┐
│ C1  │ C2  │ C3  │
└─────┴─────┴─────┘

视口 500px:放 2 列
┌────────┬────────┐
│   C1   │   C2   │
└────────┴────────┘

原理拆解:

  • minmax(240px, 1fr):每列最小 240px,最大吃掉剩余空间
  • auto-fit:浏览器自己算能放几列,塞满容器
  • repeat():把上面那个配方重复 N 次,N 由浏览器算

4.2 auto-fit vs auto-fill:一个关键区别

这两个值 90% 时候行为一样,只有在卡片数少于能放下的列数时才表现出差异:

text
容器能放下 4 列,但只有 2 张卡片

auto-fill:                       auto-fit:
┌────┬────┬────┬────┐            ┌──────────┬──────────┐
│ C1 │ C2 │    │    │            │    C1    │    C2    │
└────┴────┴────┴────┘            └──────────┴──────────┘
保留空的轨道位置                   把空轨道折叠,拉伸现有卡片
  • auto-fill:始终保留轨道占位,卡片"靠左排",剩下位置空着。
  • auto-fit:把空轨道折叠成 0,让现有卡片拉伸占满整个容器。

实战经验:99% 的卡片列表场景(图库、商品、博客)用 auto-fit——卡片少的时候让它们撑满,看起来才不"瘪"。auto-fill 更适合"占位等待填充"的场景,比如分类导航的固定格子。

4.3 为什么这比 media query 强

经典 media query 写法的痛苦:

css
.cards { grid-template-columns: 1fr; }
@media (min-width: 540px) { .cards { grid-template-columns: 1fr 1fr; } }
@media (min-width: 800px) { .cards { grid-template-columns: 1fr 1fr 1fr; } }
@media (min-width: 1080px) { .cards { grid-template-columns: repeat(4, 1fr); } }

四个断点,每次窗口大小变了还得调。minmax + auto-fit 一行搞定,而且响应是连续的,不是阶梯式跳变。


五、子项放置:span / line number / named lines

容器定义好网格后,默认情况下子项按顺序一个格子放一个。但 Grid 的真正强大在于:每个子项都能精确声明自己占哪里、占几格。

5.1 三种放置方式

方式一:用网格线编号(line number)

Grid 自动给每条线编号,从 1 开始。列线从左到右 1,2,3...,行线从上到下 1,2,3...

css
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.item-a { grid-column: 1 / 3; }   /* 从线1到线3,跨2列 */
.item-b { grid-row: 2 / 4; }      /* 从线2到线4,跨2行 */
text
       1     2     3     4
    ┌─────┬─────┬─────┐
  1 │  A        │     │
    ├─────┼─────┼─────┤
  2 │     │     │     │
    │  B            │ │  ← B 从行线 2 到行线 4
  3 │     │     │     │
    └─────┴─────┴─────┘
  4

/ 前是起点线,/ 后是终点线。终点是线不是格子,所以 1 / 3 占的是格子 1 和格子 2(两条线之间是一格)。

方式二:用 span(跨几格)

不想算终点线?用 span:

css
.hero { grid-column: span 2; }     /* 跨 2 列 */
.banner { grid-column: 1 / span 3; } /* 从线1开始跨3列 */
.tall { grid-row: span 4; }        /* 跨 4 行 */

这是仪表盘里最常用的写法——大卡片跨 2 格、小卡片跨 1 格。

方式三:命名网格线

在 grid-template-* 里用 [名字] 给线打标签:

css
.layout {
  grid-template-columns: [sidebar-start] 200px [sidebar-end main-start] 1fr [main-end];
}

.sidebar { grid-column: sidebar-start / sidebar-end; }
.main    { grid-column: main-start / main-end; }

可读性比纯数字好得多,配合 grid-template-areas 使用可以做到"全场零数字"。

5.2 z 顺序与层叠

Grid 的格子可以重叠——多个子项放到同一格,靠 z-index 决定谁在上。这意味着你可以用 Grid 做覆盖式布局(比如带遮罩的 hero 图):

css
.hero {
  display: grid;
}
.hero img       { grid-area: 1 / 1; }    /* 占据整块 */
.hero .overlay  { grid-area: 1 / 1; }    /* 同一格,盖在上面 */
.hero .cta      { grid-area: 1 / 1; align-self: end; }

三元素全挤到 1 / 1,自然层叠。这是 Grid 比 Flex 灵活的地方之一。


六、Grid vs Flexbox:何时用哪个

这是新人最常问的问题。一句话原则:

页面骨架用 Grid,组件内部用 Flex。

更细的判断:

场景用什么为什么
整页 / 大区块布局Grid二维,行列同时控制
仪表盘卡片墙Gridauto-fit + minmax 一行搞定
表单 label/input 两列Grid严格对齐两列
一行按钮 / 导航项Flex一维水平排列
图标 + 文字Flex内容驱动,不需要对齐到外层列
居中一个元素Flex(一行)Grid 也行,但 Flex 更轻
文本截断 + 尾部按钮Flexmin-width: 0 配合 flex 更顺

心智模型:

  • Flex 想:"让这些东西沿一条线排,谁多谁少由 flex-grow 决定"。
  • Grid 想:"整个区域是个棋盘,每个东西放在第几行第几列"。

它们不是互斥的——实际项目里通常是外层 Grid 容器,每个 Grid 子项内部再用 Flex 布局自己的小内容。两层职责清晰,不存在"二选一"。

举个例子:

tsx
<div className="dashboard">       {/* Grid:定义卡片墙 */}
  <div className="card">
    <div className="card-head">   {/* Flex:标题 + 操作按钮一行 */}
      <h3 />
      <button />
    </div>
    <div className="card-stats">  {/* Flex:3 个数字横排 */}
      <span />
      <span />
      <span />
    </div>
  </div>
</div>

外层用 Grid 把卡片墙摆好,内层用 Flex 处理卡片里的一行内容。这就是生产环境的正确姿势。


七、实战三例

7.1 圣杯布局(Holy Grail)

经典中的经典:header、footer、中间三栏(左 nav、中 main、右 aside)。

css
.holy-grail {
  display: grid;
  grid-template-columns: 200px 1fr 240px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav    main   aside"
    "footer footer footer";
  min-height: 100vh;
  gap: 16px;
}

.holy-grail > header { grid-area: header; }
.holy-grail > nav    { grid-area: nav; }
.holy-grail > main   { grid-area: main; }
.holy-grail > aside  { grid-area: aside; }
.holy-grail > footer { grid-area: footer; }

/* 移动端折叠 */
@media (max-width: 768px) {
  .holy-grail {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "nav"
      "aside"
      "footer";
  }
}

JSX:

tsx
<div className="holy-grail">
  <header />
  <nav />
  <main />
  <aside />
  <footer />
</div>

对比一下用 Flexbox 写同样的布局——你需要嵌套、order、media query 里改 flex-direction、再加一堆 flex-basis。Grid 版本短一半,可读性高一个量级。

7.2 仪表盘网格(不等分卡片)

仪表盘的典型需求:有几个大卡片(KPI 趋势图),几个小卡片(数字、状态),它们要拼成一张墙。

css
.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 140px;   /* 每行默认高 140px */
  gap: 16px;
}

.kpi-big   { grid-column: span 2; grid-row: span 2; }   /* 2x2 大卡 */
.kpi-wide  { grid-column: span 2; }                     /* 横长卡 */
.kpi-tall  { grid-row: span 2; }                        /* 竖高卡 */
.kpi-small { /* 默认 1x1 */ }

效果示意:

text
┌──────────────┬──────────────┬──────┬──────┐
│              │              │  S1  │  S2  │
│   kpi-big    │   kpi-wide   ├──────┴──────┤
│   (2x2)      │              │    kpi-tall │
│              ├──────┬───────┤    (1x2)    │
├──────────────┤  S3  │  S4   │             │
│   kpi-wide   │      │       │             │
└──────────────┴──────┴───────┴─────────────┘

关键技巧:grid-auto-rows: 140px 让每行固定高,这样 span 2 的卡片高度就是 140 * 2 + gap,跨行卡片之间天然对齐。这是仪表盘布局的核心套路。

7.3 自适应卡片列表(响应式神器)

最常用、最实用的一个——博客文章列表、商品墙、图库都靠它:

css
.card-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}

.card {
  /* 卡片内部用 Flex 处理垂直方向的内容 */
  display: flex;
  flex-direction: column;
  padding: 20px;
  border-radius: 8px;
}

.card-body {
  flex: 1;   /* 让所有卡片底部对齐 */
}
tsx
export function ArticleList({ articles }: { articles: Article[] }) {
  return (
    <section className="card-list">
      {articles.map(a => (
        <article key={a.id} className="card">
          <h3>{a.title}</h3>
          <p className="card-body">{a.excerpt}</p>
          <a href={`/a/${a.slug}`}>阅读全文</a>
        </article>
      ))}
    </section>
  );
}

这里有一个进阶技巧:用 flex: 1 让 .card-body 撑开。这样不管某张卡片的标题/摘要多长,"阅读全文"按钮永远在卡片底部对齐——视觉上整齐划一。

text
同一行卡片,标题长度不一:

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ 短标题       │  │ 这是一个非   │  │ 中等标题     │
│              │  │ 常长的标题   │  │              │
│              │  │              │  │              │
│ 摘要内容...  │  │ 摘要内容...  │  │ 摘要内容...  │
│              │  │              │  │              │
│              │  │              │  │              │
│ → 阅读全文   │  │ → 阅读全文   │  │ → 阅读全文   │  ← 全部对齐
└──────────────┘  └──────────────┘  └──────────────┘

这就是外层 Grid + 内层 Flex 的经典配合——Grid 管宏观排列,Flex 管微观内容。

7.4 加餐:CSS subgrid(子网格)

如果你需要让嵌套的子网格和父网格共享列轨道(比如表格状的多列卡片),传统 CSS 做不到——子项一旦进 Grid 容器就独立成网格。subgrid 解决了这个问题:

css
.parent {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.child {
  display: grid;
  grid-template-columns: subgrid;   /* 继承父网格的列定义 */
  grid-column: span 3;
}

2024 年起主流浏览器(Chrome 117+、Safari 16+、Firefox 71+)已全部支持。生产环境可以放心用——它能解决"嵌套表格卡片"这种 Grid 以前的痛点。


小结

回到本文开头的判断,带你看完 Grid 的全貌后,三条要点拎清楚:

  1. Grid 是二维系统,Flex 是一维系统。 页面骨架、卡片墙、对齐表这类"行列都要管"的布局,应该首选 Grid。一行按钮、图标+文字这种内容驱动的一维排列,留给 Flex。

  2. minmax(min, 1fr) + auto-fit 是响应式布局的核武器。 一行 CSS 实现"卡片随视口自适应列数、无 media query"。99% 的卡片列表场景用 auto-fit(不是 auto-fill)——它能让卡片少的时候撑满容器。

  3. grid-template-areas 让 CSS 自己讲清楚页面结构。 命名区域、ASCII 风格的布局声明、媒体查询里换一片字符串就能重排——这是 Grid 最被低估、最该团队推广的特性。

3 条实战经验:

  • 能用 span 就别算行号。 grid-column: span 2 比 grid-column: 1 / 3 可读、好维护。
  • 跨行卡片用 grid-auto-rows 固定行高。 仪表盘对齐的秘密就这一句。
  • 外层 Grid、内层 Flex。 大结构用 Grid 摆,小内容用 Flex 排——职责分明,互不冲突。

金句:Flexbox 解决"这一行怎么排",Grid 解决"这一页怎么布"。把它们当成两件不同的工具,而不是一个的替代品——你的 CSS 会瞬间清爽一截。

下一篇我们聊 Container Queries——元素级别的响应式,它会再次颠覆你写 media query 的习惯。

Related

相关推荐