先理解坐标系:轨道、间隙与内容盒子
Grid 的第一性概念是:容器被划分成若干「行轨道」和「列轨道」,子元素被放进由行列线交叉形成的「网格区域」里。所有尺寸计算都基于轨道的尺寸,而不是子元素自身。这一层反直觉之处在于,你写的 `1fr` 描述的是轨道有多宽,而不是子元素有多宽;子元素的宽度最终是轨道宽度减去自己的 padding、border 和 margin。
轨道之间可以有「间隙」(`gap`)。gap 不是轨道的一部分,而是轨道之间的空隙,它不会被内容占用。这也是为什么 `gap` 比给每个子元素加 margin 更合适:用 margin 模拟间隔时,容器的首尾也会出现间隔,除非再写负 margin 抵消;gap 没有这个问题,并且在容器只有一个子元素时也不会产生任何间隔。
轨道尺寸有两条确定的来源:显式定义(`grid-template-columns` / `grid-template-rows`)和隐式定义(超出显式部分后由 `grid-auto-rows` / `grid-auto-columns` 自动生成)。显式轨道排在前面,隐式轨道追加在后面,这个「显式在前、隐式在后」的顺序决定了自动放置算法往哪里放多余元素。
还有两个必须分清的概念:网格线(line)与网格区域(area)。列线从 1 开始编号,n 条列线定义 n-1 个列轨道。这也是 `grid-column: 1 / 3` 表示「从第一条线到第三条线」,也就是横跨第 1、2 两个轨道的原因——编号指的是线,不是格子。写网格时把这个偏移数错一位是最常见的低级错误,值得反复确认。
.layout {
display: grid;
/* 显式列轨道 + 轨道间隙 */
grid-template-columns: 240px 1fr;
gap: 24px;
/* 隐式行轨道:未定义的行按这个尺寸生成 */
grid-auto-rows: minmax(120px, auto);
}
/* 网格线编号从 1 开始,横跨两个轨道要写 1 / 3 */
.sidebar {
grid-column: 1;
grid-row: 1 / 4; /* 跨第 1、2、3 行 */
}
.content {
grid-column: 2;
}
/* 也可以用具名区域,先声明再用 */
.layout {
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
grid-template-columns: 240px 1fr;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }fr 的分配逻辑与溢出的真正原因
`fr` 表示的是「剩余空间的比例」,而不是「元素的宽度」。解析顺序是:先用所有非 fr 的轨道(固定长度、百分比、`auto`、`minmax()` 的固定部分)占据空间,再把容器剩余的空间按 fr 的数值比例分配。如果所有轨道都是 fr,那么分配基准就是容器宽度除以 fr 系数之和。
这条规则的直接后果是 `1fr` 不等于「一半」。在 `grid-template-columns: 1fr 1fr` 里,两列确实各占一半;但在 `grid-template-columns: 1fr 200px` 里,`1fr` 拿到的是「容器宽度减去 200px 减去 gap」之后的那一份。同时,最小尺寸约束(`min-width: auto`)会为 fr 轨道提供一个内容决定的最小值,如果内容的固有宽度超过了按比例算出来的轨道宽度,轨道就不会继续缩小——这就是很多「Grid 布局莫名横向溢出」的根因。
解决办法有两种。一是给轨道设置明确的最小值:`minmax(0, 1fr)` 告诉浏览器这个轨道不参与最小尺寸的内容驱动计算;二是给子元素设置 `min-width: 0`(或 `overflow: hidden`),覆盖掉 `min-width: auto` 的默认行为。实际项目里,把 `1fr` 无脑写成 `minmax(0, 1fr)` 是一个非常省事的习惯,能挡掉一整类溢出问题。
另一个容易踩的点是 `fr` 与 `auto` 的混合。当列定义写成 `auto 1fr` 时,`auto` 轨道的尺寸取决于它那一列里所有内容的最大固有宽度,而 `1fr` 拿剩下的。这看起来很自然,但在内容宽度不可控的场景(比如包含长 URL 或者超长单词)里,`auto` 列会把空间全部吃掉,`1fr` 列被挤到接近零。可预期的做法是给 `auto` 列也加一个上限:`minmax(0, auto) 1fr`,或者直接用 `fit-content()` 与 `clamp()` 组合出明确边界。
.app {
display: grid;
/* auto 列给出上限,避免长内容把 1fr 挤没 */
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
gap: 16px;
}
/* 保险写法:所有 1fr 都写成 minmax(0, 1fr) */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* 经典溢出场景:Grid 子项默认 min-width: auto,长单词会撑破容器 */
.grid-item {
min-width: 0; /* 关键:解除内容驱动的最小宽度 */
overflow-wrap: anywhere;
}
/* 传统 Flex 里对应的坑与解法(对比记忆) */
.flex-item {
min-width: 0; /* Flex 子项同样需要手动归零 */
}
.flex-item img {
max-width: 100%;
height: auto;
}minmax、auto-fill 与 auto-fit:响应式的三件套
`minmax(min, max)` 定义一个轨道的最小值与最大值区间。`minmax(0, 1fr)` 中,最小值 0 覆盖了自动最小尺寸,最大值 1fr 参与剩余空间分配;`minmax(200px, 1fr)` 则同时表达「不要窄于 200px」和「尽量吃掉剩余空间」。需要固定上限时用 `minmax(200px, 400px)`,需要允许收缩时用 `minmax(min-content, 1fr)`,让轨道在内容太宽时至少撑到内容宽度。
`repeat(auto-fill, <pattern>)` 会按容器宽度计算能放下多少个 `<pattern>`,然后把这个数量(哪怕最后是空轨道)作为显式列数填满。`auto-fit` 的差别在于:折叠那些没有放置任何内容的空轨道,让已有轨道撑满容器。视觉上,元素数量少于一行容量时 `auto-fill` 会留下空白而 `auto-fit` 会把元素拉宽。
一个反直觉但重要的细节:两者的差别在元素数量恰好等于或少于容器能容纳的数量时最明显;而当元素多到需要换行时,它们的表现几乎一致。另一个陷阱是容器宽度为 0 或元素被 `display: none` 时 `auto-fit` 可能计算出 0 个轨道,导致子元素全部叠在一起。所以在侧边栏收起、模态框关闭这类场景下,务必给容器一个非零的最小宽度或改用媒体查询显式声明列数。
实践中最稳的组合是「媒体查询管断点,Grid 管列数」:在窄屏用单列,桌面端用 `repeat(auto-fill, minmax(280px, 1fr))` 这种流式网格。但如果设计稿有明确的列数断点要求(首页三列、列表页五列),那就用媒体查询显式写死列数,此时不要用 auto-fill,因为设计稿的列宽通常不等分,auto-fill 的等分逻辑会对不上。
/* ① 流式卡片网格:自动换列,无需媒体查询 */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* ② 同样写法但元素少时铺满:auto-fit 会折叠空轨道 */
.toolbar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
/* ③ 显式断点:设计稿规定列数时更可靠 */
.page-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) { .page-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .page-grid { grid-template-columns: repeat(3, 1fr); } }
/* ④ 侧边栏 + 主内容区,minmax(0, 1fr) 防止内容撑破 */
.shell {
display: grid;
grid-template-columns: 0 minmax(0, 1fr);
transition: grid-template-columns 200ms ease;
}
.shell.is-open { grid-template-columns: 240px minmax(0, 1fr); }
@media (prefers-reduced-motion: reduce) {
.shell { transition: none; }
}排布:grid-auto-flow 与子项定位
默认的自动放置策略是 `grid-auto-flow: row`,也就是从上到下填满一行再换行,这在中文语境下称为「行优先」。设成 `column` 就变成列优先,从左到右填满一列再换列。信息流(瀑布流、标签列表)用哪种都可以,但两者产生的节奏完全不同:行优先适用于横向浏览的列表,列优先适用于纵向滚动的看板和看板式的分类展示。
`grid-auto-flow: dense` 会让后续子项「回填」前面由更大元素留下的空洞。对于高度不一致的卡片瀑布流,它能显著减少行尾的空白;代价是视觉顺序与 DOM 顺序不再一致,键盘 Tab 顺序也会跟着乱。只在纯粹装饰性、顺序无关的图块上使用 dense,凡是涉及交互顺序的地方都不要开。
显式定位用 `grid-row-start` / `grid-row-end` / `grid-column-start` / `grid-column-end`(可简写为 `grid-row` 和 `grid-column`),值可以是编号,也可以是 `span N`。编号为负数时从网格的显式网格末端开始倒数,这是做「粘性表头 / 粘性页脚」这类固定行非常方便的方式,因为不需要知道总行数。
最后是 `grid-template-areas`。它用字符串画出布局,视觉上最接近设计稿,调试时可读性也最好,代价是每行的列数必须严格对齐(少一个空格就会解析失败),且在循环生成列时不如编号灵活。经验是:布局固定的页面用 areas,动态或列数不定的区域用编号 + span。
/* ① 看板:列优先 + 横向滚动 */
.board {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 280px;
grid-template-rows: repeat(3, auto);
gap: 16px;
overflow-x: auto;
}
/* ② 瀑布流:行优先 + dense 回填空洞(仅装饰性图块) */
.masonry {
display: grid;
grid-auto-flow: row dense;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
/* ③ 负数网格线:无需知道总行数即可固定首尾 */
.report {
display: grid;
grid-template-rows: auto 1fr auto;
}
.report > .title { grid-row: 1; }
.report > .body { grid-row: 2; }
.report > .footer { grid-row: -1; } /* 从显式网格末端倒数第一个 */
/* ④ 具名区域:一眼看清布局 */
.page {
display: grid;
grid-template-areas:
"nav nav"
"side main"
"side foot";
grid-template-columns: 200px minmax(0, 1fr);
grid-template-rows: 56px 1fr auto;
min-height: 100vh;
}
.nav { grid-area: nav; }
.side { grid-area: side; }
.main { grid-area: main; }
.foot { grid-area: foot; }居中方案与 Grid/Flex 的分工
Grid 时代最简洁的居中就是占满一格再对格子里做居中:`display: grid; place-items: center;` 一行搞定水平和垂直居中,效果等价于旧写法里那一长串 `position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);`,而且不脱离文档流、不需要设置父元素 `position: relative`,还能撑满可用空间。
需要「在水平和垂直方向都留出等距空白」时用 `place-content: center`,它居中的是整个网格轨道组而不是网格项;`place-items: center` 居中的是每个网格项在其格子中的位置。混淆这两者是最常见的语义错误:想要「内容块居中但背景铺满」时应该用 `place-items`,想让整个网格聚成一小团居中则用 `place-content`。
Grid 和 Flexbox 的分工,判断标准其实很清晰:Grid 是二维的,适合定义「行列结构」;Flexbox 是一维的,适合在一个方向上对齐和分配。项目符号通常写在一行的开始处(`justify-content: flex-start`,即默认的 `normal`)或末尾处,不要为了「居中好看」去改写它;只有在设计上明确要求居中时才使用 `center`,并且要在代码评审时说明这是刻意的设计决策,而不是随手写上去的。
选择依据可以简化成一句话:当你要描述的是「几行几列、每格多大」时用 Grid;当你要描述的是「这一串元素怎么在主轴上排布」时用 Flexbox。最常见的实际组合是外层 Grid 划分页面区块(侧栏、主区、页脚),内层每个区块用 Flexbox 处理按钮组、标签行、表单这样的单方向排布。
/* ① 通用居中:Grid 版一行搞定,不脱离文档流 */
.center-box {
display: grid;
place-items: center; /* 网格项在其格子内水平垂直居中 */
min-height: 320px;
}
/* ② 整个网格聚成一小团居中:用 place-content,不是 place-items */
.login-page {
display: grid;
place-content: center; /* 轨道组整体居中 */
grid-template-columns: 320px;
min-height: 100vh;
}
/* ③ 经典等式:外层 Grid 分区块,内层 Flex 排单方向 */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar__spacer { margin-left: auto; } /* 把右侧操作推到最右 */
/* ④ 移动优先的两列自适应,卡片最小宽 260px */
.split {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
/* ⑤ 尊重减少动效偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}