布局能力的几次升级,方向是把”由 JS 计算尺寸”这件事逐步还给 CSS。掌握每种能力的适用边界,比记住语法重要。
一维与二维:Flex 与 Grid 的分工
Flex 是一维布局模型:沿主轴排列项目,能控制分布、对齐、伸缩,但不关心行与列是否对齐。Grid 是二维模型:同时定义行轨道和列轨道,项目直接落进单元格。
选择依据不是”哪个新用哪个”,而是你是否需要行列同时对齐。
/* 工具栏:一维排列,Flex 更直接 */
.toolbar { display: flex; gap: 8px; align-items: center; }
.toolbar .spacer { margin-inline-start: auto; }
/* 卡片网格:要求列对齐、行高一致,Grid 更合适 */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
一个常见误区是用 Flex + flex-wrap 做网格。换行后每行的项目宽度由内容决定,列无法对齐,需要给每个项目写死百分比宽度才能勉强对齐——那还不如直接上 Grid。
反过来,用 Grid 做纯线性的按钮组也是过度设计,display: flex; gap: 8px 一行就够。
显式网格与隐式网格
grid-template-columns / grid-template-rows 定义的是显式网格。项目数量超出显式定义的格子时,浏览器会创建隐式轨道来容纳它们,隐式轨道的尺寸由 grid-auto-rows / grid-auto-columns 控制,默认是 auto(由内容撑开)。
.list {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 显式:三列 */
grid-auto-rows: 80px; /* 隐式行的高度 */
grid-auto-flow: row dense; /* dense 会回填前面的空隙 */
}
dense 是个双刃剑:它能让后续小项目回填前面的空洞,视觉上更紧凑,但会打乱 DOM 顺序与视觉顺序的对应关系,键盘 Tab 导航顺序仍按 DOM 走,这对无障碍是明确的损害。内容顺序有意义的列表不要用 dense。
grid-auto-flow: column 则用于”数量不定的横向追加”场景,比如时间轴。
auto-fit 与 auto-fill 的差别
两者都用于”自动决定放几列”,差别体现在剩余空间不足时:
/* auto-fill:尽可能多地创建轨道,哪怕空的也占位置 */
.g1 { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
/* auto-fit:先创建轨道,再把空轨道折叠掉,让有内容的轨道拉伸填满 */
.g2 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
只有两个项目、容器很宽时:auto-fill 会留出一片空白(空轨道存在),auto-fit 会让这两个项目各占一半宽度(空轨道被折叠,剩余空间被 1fr 吸收)。
想要”项目始终撑满可用宽度”选 auto-fit;想要”项目保持固定宽度并左对齐”选 auto-fill。
minmax 与 fr 的配合
fr 表示剩余空间的一份。要注意它不是”比例”,而是分配剩余空间——受内容最小尺寸约束。
.grid {
grid-template-columns: minmax(120px, 1fr) 3fr;
}
这里第一列至少 120px,最多占 1 份剩余空间;第二列占 3 份。
fr 的隐式最小值是 auto,意味着内容(如一个超长不换行的单词)会把轨道撑得比预期宽。想强制按比例分配,把最小值写成 0:
.grid { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); }
minmax(0, 1fr) 是处理 Grid 内部文本溢出、子元素滚动的经典写法。下面这个场景很常见:
.card {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto; /* 中间区域可滚动 */
}
.card .body { overflow: auto; }
中间区域写 1fr 而没有 minmax(0, ...) 时,内容会把行撑开,overflow: auto 不生效。
另外 min-content / max-content / fit-content() 这几个关键字在 minmax() 里很有用,比如让某一列宽度贴合最长内容:minmax(min-content, 300px)。
subgrid:解决对齐继承
Grid 的直接子元素才会参与父网格。孙子元素属于各自父元素的独立格式化上下文,无法与外层网格对齐。这就导致”多张卡片,每张卡内部有标题/正文/按钮,希望所有卡片的标题行高度一致”这类需求,过去只能靠 JS 或固定高度。
subgrid 让子网格继承父网格的轨道:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.card {
display: grid;
grid-column: span 1;
grid-template-rows: subgrid; /* 继承父网格的行轨道 */
grid-row: span 3; /* 占三行,与内部三个区域对应 */
}
父网格定义行,卡片内部的元素直接落进这些行,跨卡片的同一行自动共享高度。
使用要点:
– 必须同时指定 grid-row: span N,否则子网格没有可继承的轨道范围;
– subgrid 可以只继承一个方向(grid-template-rows: subgrid 而列独立定义);
– 继承方向上的 gap 默认从父网格带下来,可以用 gap 覆盖。
容器查询与媒体查询的本质区别
媒体查询看的是视口,容器查询看的是祖先容器的尺寸。
.sidebar { container-type: inline-size; container-name: sidebar; }
@container sidebar (min-width: 400px) {
.widget { display: grid; grid-template-columns: 1fr 2fr; }
}
这个差别带来的能力是组件自适应:同一个卡片组件放在宽侧边栏里显示横向布局,放在窄列表里显示纵向布局,无需 JS 测量,也无需知道自己在页面的哪个位置。
几个必须注意的点:
container-type: inline-size会让该容器在内联方向上建立尺寸包含(size containment),意味着它的宽度不再由内容撑开——一个inline-size容器必须有外部给定的宽度,否则会塌缩为 0。这是最常见的踩坑。container-type: size同时在两个方向建立包含,要求宽高都由外部决定,使用条件更严。- 容器查询不能查询自身——规则必须写在容器的后代上,写在容器本身会被忽略(避免循环依赖)。
- 容器查询的单位
cqw/cqh/cqi/cqb可以像vw一样用在任意属性上:
.card-title { font-size: clamp(1rem, 4cqi, 1.5rem); }
配合 style() 查询还能基于自定义属性做条件分支,不过它目前只能查询自定义属性的值,不能查询普通属性。
:has() 带来的选择器能力
:has() 是”父选择器”和”前置兄弟选择器”的实现,它让 CSS 能基于后代或后续兄弟的状态来改变元素自身样式。
/* 表单项里有错误提示时,整个表单项变红 */
.field:has(.error) { border-color: crimson; }
/* 卡片里没有图片时,改变内边距 */
.card:not(:has(img)) { padding-block: 32px; }
/* 前一个元素被选中时高亮后一个 */
.item:has(+ .item.selected) { background: #f5f5f5; }
它替代了一大批原本需要 JS 加 class 的逻辑:校验失败态、内容缺失态、兄弟项联动。
性能上要注意::has() 让选择器匹配不再是纯自底向上,浏览器需要额外验证后代状态。写法上把 :has() 放在最具体的那一侧能减少匹配范围:
/* 较差:先匹配所有 li 再验证 */
li:has(a.active) { }
/* 较好:限定在已知容器内 */
.nav li:has(a.active) { }
避免在 :has() 内部使用过于宽泛的后代选择器(如 :has(*));也不要在超大量的元素列表上叠加多层 :has()。实际上手时,用 DevTools 的样式失效分析或性能录制确认没有引入明显的样式重算开销。
逻辑属性与书写模式
margin-inline-start 这类逻辑属性按书写模式(writing-mode / direction)解析方向,而不是物理左右。做国际化布局时用逻辑属性能自动适配 RTL:
.card { padding-inline: 16px; margin-block-end: 8px; }
对应关系:inline 是文本行进方向(横向书写时是水平),block 是块堆叠方向(横向书写时是垂直);start / end 替代 left / right。
Flex 的 justify-content 与 Grid 的 justify-items 同样遵循这套方向,改 direction: rtl 后会自动镜像,不需要重写布局代码。
瀑布流的现状
瀑布流(masonry)在部分浏览器里通过 grid-template-rows: masonry 实验性支持,规范尚未稳定。生产环境仍要靠 JS 计算列高,或用 CSS columns 做近似。
columns 的局限是内容按列填充(先填满第一列再填第二列),阅读顺序与视觉顺序不一致,对内容有先后要求的列表不适用。
只需要”等高卡片加自动换行”的话,Grid 的 auto-fit 已经够用,不必上瀑布流。
落地时的几条约束
- 布局能用 CSS 表达就别用 JS 测量,
ResizeObserver应该是最后手段; - Grid 里的滚动区域记得配
minmax(0, 1fr); dense与无障碍冲突,谨慎使用;- 容器查询前先确认浏览器支持基线,不支持的要有合理退化(一般是单列布局);
:has()的复杂规则加注释说明意图,它的可读性比普通选择器低。
Comments (2)
这篇应该进新人培训材料。
看完觉得自己又可以了(明天就会被打回原形)。
Please Sign in to join the discussion.