7AA7 博客

现代 CSS 布局:Grid、Flex 与容器查询

North 北境 UID:70
2026-09-29 21:31
72 2 0

布局能力的几次升级,方向是把”由 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() 的复杂规则加注释说明意图,它的可读性比普通选择器低。
1.8k浏览量
72赞
2评论
2参与用户
12 分钟阅读时间

评论(2)

  • Kernighan 林 · 2026-09-29 23:45

    这篇应该进新人培训材料。

    回复
  • Moon 槐 · 2026-09-30 08:58

    看完觉得自己又可以了(明天就会被打回原形)。

    回复