你以为卡片底部对齐只能靠 JS 算高度?今天 Subgrid 把这件事彻底原生化了
写过三年 CSS Grid,你一定遇到过这个场景:三个卡片排成一行,各自内容不一样多,有的标题长、有的摘要短,但设计师要求所有卡片的标题对齐、按钮对齐、底部对齐。
传统解法是什么?要么给标题套 `min-height`,内容一多就破;要么在卡片里写 `display: flex; flex-direction: column; margin-top: auto`,Footer 是对齐了,但标题和正文还是各自跑;要么——最绝的——写一段 JS,监听窗口 resize,算出所有卡片里最高的那一项,然后把其他卡片的高度同步过去。
这些办法有一个共同的问题:**它们都是在打补丁,而不是在解决问题本身。** 真正的问题是:嵌套网格里的元素,根本不知道外层网格的行轨道长什么样。
CSS Subgrid 就是来解决这个根问题的。
**它怎么工作的**
看这个结构:
“`html
-
短标题
一段简介。
-
这是一个特别长的标题会换行
这段简介稍微长一点,但也不算太夸张。
-
中
简短的。
“`
没有 Subgrid 的时候,三个 `
加上 Subgrid,只需要两层声明:
“`css
/* 父级:定义列布局 + 显式行轨道 */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 四行轨道:标题 | 摘要 | 内容区 | 按钮 */
grid-template-rows: auto auto 1fr auto;
gap: 1.5rem;
}
/* 子级:继承父级的行轨道 */
.card {
display: grid;
grid-row: span 4; /* 占据父级的4行轨道 */
grid-template-rows: subgrid; /* 把这4行传递给孩子 */
gap: 0; /* 用父级的gap,或单独覆盖 */
padding: 1rem;
}
/* 卡片内部的 h3 / p / button 各占一行 */
.card h3 { } /* → 父级第1行 */
.card p { } /* → 父级第2行 */
.card button { } /* → 父级第4行(永远在底部) */
“`
关键就在 `grid-template-rows: subgrid` 这一行:子网格不再计算自己的行轨道,而是直接拿父级已经定义好的那几行。父级网格的第1行有多高,所有卡片里的第1行就有多高——哪怕标题文字行数不同。
这就是 Subgrid 的本质:**让嵌套网格的元素参与到外层网格的轨道计算中,而不是自顾自地重新算一套。**
**它能替代哪些老办法**
**1. 卡片底部对齐的 `margin-top: auto` 方案**
“`css
/* 之前:靠 flex 把按钮顶到底部 */
.card {
display: flex;
flex-direction: column;
}
.card button {
margin-top: auto; /* 只有按钮对齐,标题正文不行 */
}
“`
Subgrid 之后:父级行轨道直接决定每行的高度,按钮天然在底部,不需要任何 hack。
**2. JS 高度同步脚本**
以前为了保证所有卡片行对齐,业界写过大量这类代码:监听 resize、遍历所有卡片、算 max-height、赋值。现在浏览器原生支持了,这些脚本可以全部删掉。
**3. `display: contents` 把子元素「提」到父级**
有人会借助 `display: contents` 把卡片内部元素拉到父级网格上下文里——但这个属性对无障碍有影响,屏幕阅读器会丢失语义。Subgrid 不用动 HTML 结构,不破坏可访问性。
**两个方向的继承**
Subgrid 可以只继承行、只继承列,或者两者同时继承:
“`css
/* 继承行轨道(最常见) */
grid-template-rows: subgrid;
/* 继承列轨道(表单场景) */
grid-template-columns: subgrid;
/* 同时继承(复杂场景) */
grid-template-rows: subgrid;
grid-template-columns: subgrid;
“`
表单场景的典型用法:标签和输入框分别占据两列,多个表单项各自内部对齐:
“`css
.form {
display: grid;
grid-template-columns: 140px 1fr;
gap: 0.75rem 1.5rem;
}
.form-field {
display: grid;
grid-template-columns: subgrid; /* 继承父级的两列 */
grid-column: 1 / -1;
}
“`
每个 `.form-field` 的第一列永远是标签位,第二列永远是输入框,不管标签文字有多长。
**三个要注意的坑**
**坑1:`grid-row: span N` 必须和父级轨道数量匹配**
如果父级只有3行轨道,而你在子级写了 `grid-row: span 4`,多余的元素会溢出到第4行轨道去——而这一行根本不存在。调试时可以先把 `grid-row: span 4` 注释掉,看元素是否正常落入各自轨道。
**坑2:Gap 继承但可以覆盖**
Subgrid 默认继承父级的 `gap`,如果想取消或改小,在子级单独写 `gap: 0` 或其他值即可。需要注意的是,覆盖 `gap` 只影响子级内部元素之间的间距,不会改变它们和外层网格的对齐关系。
**坑3:渐进增强必须做**
Safari 16+、Chrome 117+、Firefox 71+ 支持,全球覆盖率 90%+,但仍有少量旧版浏览器用户。在子级提供 fallback:
“`css
.card {
display: flex; /* fallback */
flex-direction: column;
}
@supports (grid-template-rows: subgrid) {
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
}
}
“`
不支持 Subgrid 的浏览器下,卡片退化为普通流式布局,内容仍然可见,只是跨卡对齐失效——比整块布局崩溃要温和得多。
**下一步:把一个卡片组件改成 Subgrid**
打开你项目里最常用的卡片组件,按这个顺序改:
1. 父级容器加 `grid-template-rows` 显式声明(不要写 `auto`,否则轨道高度仍然由内容决定)
2. 子级卡片加 `display: grid; grid-row: span N; grid-template-rows: subgrid`,N 要和父级轨道数一致
3. 删掉原来用来对齐的 `min-height`、`margin-top: auto`、或者 JS 高度同步脚本
4. 用 `@supports` 包一层,不支持的浏览器下保留旧的 flex fallback
改完跑一下不同内容量的测试用例——标题三行、标题一行、摘要长短不一——看对齐是否始终正确。Subgrid 的价值在这种差异场景下最明显:以前要靠 JS 才能做到的事,现在纯 CSS 一行就稳了。
评论区
登录后可评论。