Flexbox 与 Grid 实战
两种心智模型空间分配与圣杯布局对照
Flexbox 与 Grid 实战
之前写HTML 笔记和CSS 笔记的时候,把选择器、盒模型、定位和浮动过了一遍,弹性盒子与网格布局只是点到为止。那是「第一课」,管的是元素怎么摆进文档流。
这一课专门讲现代布局的两件套:Flexbox 与 Grid。
它们不是「旧布局的替代品」,而是两套心智模型不同的系统。一个从内容出发沿一条轴协商尺寸,一个从版面出发先把二维网格画好再填内容。
把两种模型的分界想清楚,绝大多数「该用哪个」的犹豫会自动消失。
动手版本:Flexbox/Grid 布局生成器、CSS 生成器、CSS 工具箱。
一维与二维
内容优先与布局优先
Flexbox 的定位是一维布局。子项沿一条主轴排布(行或列),每个子项占多宽,是内容与弹性规则协商出来的结果。容器只提供规则,尺寸由内容参与决定。
Grid 的定位是二维布局。先声明一个有行有列的网格结构,再把子项放进格子里。格子先于内容存在。
这两个方向决定了它们各自擅长的问题。
一排按钮、一条导航、一组标签。内容多少不定、要求「挤在一起还别变形」的,是 Flex 的主场。
整页骨架(页头、侧栏、主区、页脚)、图片画廊、仪表盘。「哪里放什么」事先确定的,是 Grid 的主场。
所以「Flexbox 和 Grid 该用哪个」是个伪问题,真问题是这一层的布局由内容驱动还是由版面驱动。
成熟的页面几乎总是两层都用。页面骨架用 Grid 划出主区与侧栏,主区内部的卡片行、工具条用 Flex 排。
一个常用的自检问句:如果我删掉一个子项,其余子项应该「重新协商位置」(Flex 的语义)还是「留在原地、留个空位」(Grid 的语义)?答案直接给出选择。
心智模型错位的两种典型症状
两种典型反模式都源于模型错位。
第一种是用 Flex 硬画二维版面。导航、侧栏、主区全塞进一个 flex 容器,然后嵌套三层 div、用对齐属性一处处掰。本质是在用协商机制做声明式布局,每加一个模块都要调整前面所有模块的弹性参数。
第二种是用 Grid 排内容不定的行。标签云做成固定列数网格,标签长短不一时格子大量留白。而 Flex 的自动换行恰恰是为这种内容驱动的排布设计的。
症状记住,倒推原因即可。
Flexbox 的关键机制
空间分配的三步算法
Flex 的核心是一套空间分配算法,理解它比背属性名有用得多。
每个子项有三个参数。flex-basis(理想尺寸的起点)、flex-grow(容器有富余时按什么比例瓜分富余)、flex-shrink(容器不够时按什么比例摊派亏空)。
分配过程三步走。
定基:按 flex-basis(默认 auto,即回退到内容的尺寸)给每个子项预分配尺寸。
分余:flex-basis 总和小于容器,差值作为「富余空间」,按 grow 的比例分给各子项。grow 为 0 的子项不参与。
摊亏:总和超出容器,超出部分按 shrink 与 basis 的乘积加权摊回,shrink 为 0 的子项不缩。
由此能解释两个高频困惑。
第一,flex: 1 和 flex: auto 的区别。
flex: 1 展开是 1 1 0%。基点归零,所有子项真正「等分」容器。
flex: auto 是 1 1 auto。基点是内容尺寸,富余部分才均分,所以内容长的子项最终更宽。
「等宽三栏」用 flex: 1,「各占内容大小、剩余均摊」用 flex: auto,差一个字行为不同。
第二,臭名昭著的 min-width: auto 陷阱。
flex 子项的最小尺寸默认是 auto,即不小于内容的 min-content。这意味着一个放着长 URL 或长代码块的子项,shrink 再大也缩不下去,把整行撑破。
解法是显式 min-width: 0(或对文本用 overflow: hidden 配合)。凡是「flex 子项压不扁」的问题,九成是它。
对齐、换行与 Flex 刻意没有的东西
主轴对齐 justify-content、交叉轴对齐 align-items,再加 align-self 单点覆盖,是 Flex 对齐的三板斧。
gap 属性给子项之间统一加缝,比早期的负 margin 技巧干净一个时代。
另一个常被忽略的技巧是 margin: auto。auto 外边距在 flex 容器里会吃掉全部剩余空间。所以 margin-left: auto 就是「把它推到主轴尽头」,导航栏右侧那组按钮不用包一层容器就能靠右。margin: auto 则是双向居中。比定位加 transform 的居中方案干净得多。
多行场景由 flex-wrap: wrap 开启,配合 align-content 管行与行之间的分配。
同样值得知道的是 Flex 刻意没有的东西。它没有网格线,子项不能「跳到第几列第几行」。换行后行高各行独立,无法跨行对齐成一格一格。
这不是功能缺失而是定位使然。一维协商模型一旦允许任意跳位,就退化成另一个 Grid。
顺着这个思路还能理解 order 属性的警戒线。它能改子项的视觉顺序,但读屏与 Tab 焦点仍按 DOM 顺序走。重排幅度大的场景(把侧栏挪到前面)应该回到 DOM 顺序或 Grid 来解决。
轴系参数多、组合抽象,空想容易绕晕。在Flexbox/Grid 布局生成器里把 direction、justify、align、wrap 的组合逐个拨一遍,配着可视化结果读生成代码,轴的概念一次就焊牢。
Grid 的关键机制
fr、repeat 与一行代码的响应式
Grid 的声明式特性集中在轨道定义上。
grid-template-columns: 200px 1fr 2fr 的意思是:先给第一列 200 像素,剩余空间按 1:2 分给后两列。fr 是「剩余空间的份数」,区别于百分比(百分比相对容器全宽,fr 只分剩下的)。
隐式网格是另一层兜底。子项数量超出显式声明的轨道时,Grid 自动补造行,尺寸由 grid-auto-rows 控制。所以一个只声明列的网格也能自动往下长。
响应式卡片布局的惯用一行:
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px;}语义逐词拆:每列最小 240px、最大均分剩余。auto-fit 让列数随容器宽度自动增减。宽了三列、窄了两列,一行媒体查询都不用写。
auto-fit 与 auto-fill 的差别在「列数富余时」。fill 老老实实保留空轨道,fit 把空轨道折叠掉、让现有列吃满整行。
列表页想「就算只有两张卡片也撑满宽度」用 fit,想「卡片永远不超宽、右侧留白」用 fill。
这一行代码是 Grid 表达力的浓缩:媒体查询管断点,Grid 管连续适应。把大量本该手写的断点逻辑收回容器声明里。
网格线、区域命名与对齐词汇
Grid 的底层坐标是网格线。n 列的网格有 n+1 条列线,从 1 起数,-1 是最后一条。
子项可以按线定位(grid-column: 1 / 3 即从 1 号线跨到 3 号线)、按跨度定位(grid-column: span 2)。
这也是 Grid 的隐藏福利:子项的 DOM 顺序与视觉顺序解耦。小屏时把侧栏挪到主区下面,不用动 HTML,改两行 grid 属性即可(读屏顺序仍跟 DOM,重排别破坏语义顺序)。
当版面结构稳定时,区域命名是最可读的写法:
.page { display: grid; grid-template-areas: "header header" "main aside" "footer footer"; grid-template-rows: auto 1fr auto; grid-template-columns: 1fr 280px;}.page > header { grid-area: header; } 依此类推。模板字符串本身就是版面的等比例草图,改版面就是改这张「图」。
对齐在 Grid 里有一整套更细的词汇,和 Flex 的三板斧同名但管辖范围更大。
justify-items/align-items 管子项在各自格子内部的摆放。justify-content/align-content 管整组轨道在容器里的分布(轨道总尺寸不足容器时才有戏)。place-items: center 一行顶两行。
名字相同的属性在两套系统里管辖的对象不同。Flex 里 justify-content 管的是「子项」,Grid 里同一属性管的是「轨道」。这份一词多义是两套系统互抄属性名的副产品,用错了先检查自己站在哪套模型里。
容器查询与流式刻度
auto-fit 解决的是「列数随容器变」,但组件内部的布局还只能随视口变。媒体查询只认识窗口宽度,同一个卡片组件放进宽侧栏和窄主区,它自己无法感知处境。
容器查询(container queries)补的就是这一层。先在容器上声明 container-type: inline-size,再用 @container (min-width: 480px) { … } 让组件按自己所在容器的宽度切换布局。
这与 Grid 的分工天然咬合。骨架层用 Grid 把空间分给各个区域,每个区域里的组件用容器查询自适应自己的新家。组件第一次拥有了「放哪儿都长成该有的样子」的能力,设计系统里的卡片、表单行因此不必再为断点写全局特例。
同一思路的流式版是 clamp()。font-size: clamp(1rem, 0.5rem + 2vw, 1.5rem) 在下限与上限之间随视口连续滑动。width: clamp(240px, 50%, 480px) 让尺寸永远不出界。
它和 minmax() 是同一个哲学在不同层的表达:把「临界值之间的连续过渡」声明出来,而不是用断点切成阶梯。
两者叠加之后,媒体查询的剩余职责就只剩真正的结构性切换(单栏变双栏、导航变抽屉)。大多数「宽度适配」类需求都可以在组件层就地消化。
这也反过来改变了写组件的习惯。先假设组件可能被放进任意宽度的容器,再决定哪些行为交给容器查询、哪些交给 Grid 的轨道。断点从「全局常量」降级成「最后手段」,是这套能力到位后最直接的风格迁移。
同一布局的两种实现
圣杯布局
经典的圣杯布局(页头、主区、侧栏、页脚)用 Grid 写是纯声明:
/* Grid 版 */.page { display: grid; grid-template-areas: "header header" "main aside" "footer footer"; grid-template-rows: auto 1fr auto; grid-template-columns: 1fr 280px; min-height: 100vh;}四个子项各自认领 grid-area,版面即字符串,1fr 让主区吃掉剩余高度。
用 Flex 写则要两层容器。外层纵向排头尾,中层横向排主区与侧栏:
/* Flex 版 */.page { display: flex; flex-direction: column; min-height: 100vh; }.middle { display: flex; flex: 1; }.main { flex: 1; min-width: 0; }.aside { width: 280px; flex-shrink: 0; }注意 Flex 版 min-width: 0 那一行不是装饰。主区一旦放进长代码块,没有它整行就被撑破,正是上一节那个陷阱的现场出镜。
两版功能等价,但 Grid 版把结构写在容器一处,Flex 版的结构分散在两层容器的属性里。若需求变成「中屏时侧栏挪到主区上方」,Grid 版改模板字符串一行,Flex 版要重新组织 DOM 层级。
反过来,一旦圣杯里的「主区」内部还有一排不定长的工具条按钮,那层的排版 Flex 又是明显更自然的选择。骨架归 Grid,内容流归 Flex,在这组对照里得到最直观的印证。
粘底页脚这道常考题同理。Grid 版直接声明三行轨道 auto 1fr auto,中间行吃掉全部剩余高度。Flex 版靠 margin-top: auto 的「推到底」语义。
两版都只要三五行。但 Grid 版不需要给页脚本身加任何属性。布局意图完全由容器表达,也就是声明式布局的精神所在。
从原型到代码
把方法论落到一个具体组件:博客文章列表页,桌面端双栏(正文列表加侧栏),移动端单栏,卡片头部分布着标题与日期。
推演顺序从最外层开始。
第一问「页面骨架是二维的还是一维的」。双栏加页脚,二维,容器上 Grid,写 grid-template-columns: minmax(0, 1fr) 280px。minmax(0, 1fr) 而不是裸 1fr 是 Grid 版的防撑破写法(1fr 默认最小尺寸同样是内容尺寸,格子里放长代码同样会撑破)。移动端把列模板改回单列、侧栏排到下面,一个断点解决。
第二问落到卡片列表本身。卡片数量不定、宽度该随容器连续适应。上一节那行 repeat(auto-fit, minmax(240px, 1fr)) 直接搬来。
第三问进入卡片内部。标题左、日期右、底部一排标签。一维内容流,Flex。标题过长时给标题容器 min-width: 0 配合省略号。
三层各归其位,总共不到三十行 CSS,没有一个绝对定位。
这个推演顺序(先问维度,再问内容是否驱动,最后才写属性)可以复用到几乎任何布局需求上。
自动放置与两个进阶开关
不写 grid-area 的子项会按自动放置算法依次填进格子。默认 grid-auto-flow: row,逐行从左到右。
它有个值得认识的变体 dense。普通模式下,某个大块子项放不进当前行尾的碎片空间就会换行,留下空洞。dense 允许后面的小块子项回头填这些空洞。
图片瀑布流用它可以消除「高低不齐导致的窟窿」,代价是视觉顺序进一步偏离 DOM 顺序。含交互内容的列表别用,纯展示画廊可以考虑。
另一个常用开关是 display: contents。让某元素自己的盒子消失、子项直接参与父级的网格。适配「组件包裹层破坏网格结构」的场景(框架渲染多出来的包装 div)。但它会抹掉该元素自身的语义与可访问性角色,仅对纯结构性包装使用。
Grid 层面还有子网格(subgrid)可以把父网格的轨道延续进嵌套结构,处理「卡片标题区跨卡片对齐」这类需求。支持度已可用的今天,遇到「嵌套网格怎么都对不齐」的问题,先想起它。
常见误区五则
- **用 padding 当 gap。**子项各自的 padding 加父容器负 margin 的老技巧,会让悬停区域、点击区域跟着变形。现代浏览器
gap在 Flex 与 Grid 里都可用,直接用。 - 写死列数。
repeat(3, 1fr)在窗口变窄时不出错也不会变,它只是死掉。需要自适应列数的场合一律auto-fit加minmax。 - 在 flex 容器上用 text-align 期待居中。
text-align只管行内内容的文本对齐,管不到 flex 子项的排布。主轴居中是justify-content的事。同理,float 在 flex/grid 子项上会被忽略。旧布局的肌肉记忆在这两套系统里整体失效。 - 百分比高度为什么失效。
height: 50%需要父级有确定高度,链条上一旦断了就退化为 auto。Grid 的行高用fr或minmax在容器层声明,多数「百分比高度不生效」的问题应改为在容器层分配轨道。 - 拿 transform 当布局。
transform: translate只改绘制不改布局,占位还在原地。用它做居中微调可以,做结构位移会留下幽灵占位和滚动条问题。定位结构的事,交给布局系统。
布局之外
现代布局还有一组容易被当作「新命名风格」而低估的属性:逻辑属性。
margin-left、padding-top 这类是物理属性,写死了东南西北。逻辑属性改用相对书写的方向词。margin-inline-start(行内起始侧,中文从左往右即左侧,阿拉伯语界面自动变成右侧)、padding-block-end(块方向末端,横排即下方)。
它与布局系统的关系在多语言站点上才真正显形。一套布局要在中英日阿之间切换,物理属性需要整套方向镜像覆盖,逻辑属性则跟随书写模式自动翻转。配合 direction: rtl 或 writing-mode: vertical-rl 一次都不用改。
本课推演例子里写的 margin-left: auto,在需要支持 RTL 的产品里就应写成 margin-inline-start: auto。
养成「先问自己这是行内方向还是块方向」的习惯,是把布局从「某一种语言的样子」升级成「自适应书写系统」的最后一步。布局系统负责把盒子排进网格,逻辑属性负责让「左」和「右」这两个概念不再写死。
生成器的学习价值与调试三步
最后谈谈工具怎么用才真的长本事。
第一层用法是当速查表。记不清对齐属性六个值的差别时,在Flexbox/Grid 布局生成器里拨一遍,可视化结果和对应 CSS 同屏出现,比翻文档快。
第二层用法是当参数-效果映射器。阴影的半径、模糊与扩散,圆角的大小,颜色与透明度这类纯数值参数,在CSS 生成器里拖动滑块观察变化,参数与渲染结果之间的映射关系就建立起来了。这类知识读十遍文档不如拖十下滑块。
第三层是当阅读材料。CSS 工具箱这类工具会生成玻璃拟态、新拟态、clip-path 形状这类「组合技」的完整样式。把生成的代码读下来、逐个属性注释它在整体效果里的作用、然后关掉工具手写复现。这三步走完,工具产出的就不只是代码,而是结构性的理解。
反过来要警惕的用法只有一个:把生成器当拐杖,每次都复制粘贴而从不回读。生成器给出的是答案,学习发生在追问「为什么是这个答案」的间隙里。
调试布局也有固定的三步回路。
第一步开开发者工具的 Grid/Flex 叠加层,直接把网格线、轨道尺寸和轴方向画在页面上,布局为什么长这样一目了然。
第二步查盒模型面板,确认间距到底来自 margin、padding 还是 gap。三者在叠加层里颜色不同。
第三步做减法,把可疑属性逐个关掉看变化。尤其怀疑 min-width: auto 之类的隐式行为时,显式写一个 min-width: 0 对比。
布局问题的答案几乎总在「哪一层产生了这个尺寸」这一问里,工具只是把这一问可视化。
多人协作的项目里,布局还要立几条团队级的约定,否则每个人都会重新发明一遍间距体系。
其一,间距取刻度值。4 与 8 的倍数构成一套有限刻度,gap、padding、margin 只从刻度里取。布局的「整齐感」多半不是玄学,而是间距遵守了同一套节奏。
其二,布局意图用语义命名。.sidebar-layout、.card-row 表达意图。.flex-gap-12 这类实现细节命名会让重构寸步难行。
其三,断点数量克制。全站统一两三个断点,优先靠 auto-fit 这类连续适应消化中间宽度。断点越多,测试矩阵指数膨胀,而 Grid 恰恰能把多数断点需求吃掉。
约定本身不产生能力,它产生的是可预测性。任何一个页面打开审查元素,间距与结构都长着同一张脸。
布局学习的路径到这里可以收拢。第一课的盒模型与文档流告诉你「元素本来会怎样」,这一课的 Flex 与 Grid 给你「让它怎样」的两套声明系统。
一维协商用 Flex,二维声明用 Grid,骨架与内容各归其位,断点只留给真正的结构切换。剩下的熟练度,都藏在自己动手拆掉又重搭的那几十个布局里。