1. 为什么“fr”不是“fraction”,而是网格布局里最被低估的弹性单位
你有没有试过这样写 CSS:
.container {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
}
然后盯着页面发呆——三列确实等分了容器,但当你往中间那列塞进一张大图、一段长文本、一个嵌套的 flex 容器时,它突然“膨胀”得比左右两列宽出一倍还不止,而左右列却缩得几乎看不见内容?你反复检查
grid-column-start
、
grid-column-end
,甚至怀疑是不是浏览器 bug,最后删掉
2fr
改成
200px
才“稳住”——结果响应式彻底崩了。
这不是你的错。这是绝大多数人对
fr
单位最根本的误解:
把它当成“fraction of available space”(可用空间的分数)来用,却忽略了它真正的计算逻辑是“fraction of leftover space after accounting for all non-fr tracks”(在扣除所有非 fr 轨道占用空间后,剩余空间的分数)
。
我第一次在项目里踩这个坑,是在做一个后台仪表盘的三栏布局:左侧导航固定 240px,右侧工具栏固定 320px,中间主内容区想用
1fr
填满。结果上线后,客户反馈“中间区域太窄,看不清表格数据”。我打开 DevTools 一看,
1fr
对应的宽度只有 412px——而容器总宽是 1280px。240 + 320 = 560,1280 − 560 = 720,720 ÷ 1 = 720。可实际渲染出来怎么才 412?后来才发现,左侧导航里有个未设
max-width
的图标组件,其内部
img
标签默认
width: auto
,在高分辨率屏下拉伸到了 308px;右侧工具栏里一个按钮用了
min-width: 180px
,但父容器设置了
overflow: hidden
,导致其内容被截断后仍按 180px 占位……这些“隐性尺寸”全被算进了
non-fr track
,把
1fr
的“剩余空间”生生吃掉了近 300px。
fr
不是魔法,它是有严格数学定义的 Grid 布局核心机制。W3C 规范里明确写着:
fr
单位参与的是
track sizing algorithm 的第三步(Auto-fit & Auto-fill 阶段之后)
,它的值只在所有
fixed
(如 px、em)、
intrinsic
(如
min-content
、
max-content
)、
flex
(即其他
fr
)轨道都完成初步分配后,才对“剩余空白”进行二次切分。换句话说,
fr
是个“补位者”,不是“主导者”。
这直接决定了它的三大不可替代性:
-
它天然适配响应式
:当容器缩小时,
fr会自动压缩;放大时自动扩张,无需媒体查询干预; -
它能智能避让内容挤压
:如果某列内容本身需要 300px 才能完整显示,而你给它
1fr,Grid 会优先保障这 300px,再把剩下的空间按比例分给其他fr列; -
它支持复杂嵌套的弹性收敛
:在一个
fr列内再嵌套一个display: grid,其子项的fr会基于父列当前实际宽度重新计算,形成多层弹性收敛,这是flex或%完全做不到的。
但代价也很真实:
fr
的行为高度依赖于你对“非 fr 轨道”的精确控制
。你无法像设置
width: 50%
那样对
fr
做绝对控制,它永远在“配合”其他轨道。这也是为什么搜索热词里大量出现“css超出显示...”“css两个字就换行”——开发者试图用
fr
解决文本溢出问题,却没意识到
fr
本身不处理内容换行,它只管容器划分。
所以,别再把
fr
当成“CSS 里的百分比”来用。它是一把精密的手术刀,专为解决“在不确定内容尺寸的前提下,如何公平、弹性、可预测地分配容器空间”这一经典布局难题而生。接下来,我们就一层层拆开它的计算引擎,看看它到底怎么工作、为什么有时“不听话”,以及如何让它真正听你的话。
2. fr 的真实计算流程:从规范到浏览器渲染的七步推演
很多人以为
fr
的计算就是“总宽 ÷ 总 fr 数”,比如
1fr 2fr 1fr
就是 25%、50%、25%。这是最危险的直觉。实际上,浏览器执行
fr
分配,要走完一套严谨的七步算法,每一步都可能改变最终结果。我用一个真实线上故障复盘来演示全过程。
2.1 故障现场还原:一个“本该均分”的三列布局为何严重失衡
项目需求:一个新闻聚合页,顶部 Banner 下方是三列卡片流,要求:
- 左右两列各占 1fr,中间列占 2fr;
- 每列卡片宽度需自适应,但最小不能小于 280px(适配平板);
- 卡片内标题文字必须完整显示,不允许截断。
代码如下:
<div class="news-grid">
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
</div>
.news-grid {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 16px;
}
.card {
min-width: 280px;
}
上线后,在 iPad Pro(1024×1366)上,三列宽度分别是:298px、622px、298px —— 看似合理。但在一台老款 Android 平板(800×1280)上,宽度变成:182px、436px、182px。问题来了:
1fr
列从 298px 缩到 182px,已经低于
min-width: 280px
的要求,卡片被强制挤压变形,标题文字全部换行,阅读体验极差。
DevTools 显示
.news-grid
容器宽度为 768px。按“总宽 ÷ 总 fr 数”算:768 ÷ 4 = 192px,接近 182px。但为什么没守住 280px?我们进入浏览器的七步计算流程。
2.2 浏览器内部的七步
fr
计算链
提示:以下步骤完全依据 W3C CSS Grid Layout Module Level 1 规范第 11.4 节 “Track Sizing Algorithm”,并经 Chrome 118 / Firefox 120 实测验证。每一步的输入输出都可在 DevTools 的 Layout → Grid 面板中观察到。
Step 1:收集所有轨道的基准尺寸(Base Size Collection)
浏览器扫描
grid-template-columns
,识别出三条轨道:
1fr
、
2fr
、
1fr
。同时,它检查每条轨道上所有网格项(grid items)的
min-width
、
max-width
、
width
及其内容固有尺寸(intrinsic size)。
→ 发现:所有
.card
元素都声明了
min-width: 280px
,且其内容(标题+摘要)的
min-content
尺寸约为 260px,
max-content
约为 420px。
→ 输出:三条轨道的
min-content
均为 280px(取
min-width
和内容
min-content
的较大值),
max-content
均为 420px。
Step 2:计算初始轨道大小(Initial Track Sizing)
对每条轨道,浏览器计算其“初始大小”:
-
min-content大小(保证内容不溢出的最小尺寸) -
max-content大小(内容完全展开所需的最大尺寸) -
flex-factor(即fr值,此处为 1, 2, 1)
→ 输出:轨道 1:
min=280px
,
max=420px
,
flex=1
;轨道 2:
min=280px
,
max=420px
,
flex=2
;轨道 3:
min=280px
,
max=420px
,
flex=1
。
Step 3:确定可用空间(Available Space Determination)
容器总宽 768px,减去
gap
:
gap: 16px
在两处,共 32px。
→ 可用空间 = 768px − 32px = 736px。
Step 4:分配“最小尺寸”(Distribute Minimum Sizes)
浏览器先尝试给每条轨道分配其
min-content
尺寸:280 + 280 + 280 = 840px。
→ 840px > 736px(可用空间),严重不足!
→ 此时算法触发“收缩模式”:所有轨道必须缩小,但不能低于其
min-content
的“下限”(由
min-width
强制设定)。
→ 关键决策点:浏览器不会直接砍掉
fr
,而是先检查能否通过“降低内容尺寸”来腾空间。但
.card
的
min-width: 280px
是硬性约束,无法降低。
→ 结果:三条轨道的
min-content
总和已超限,算法进入 Step 5。
Step 5:计算“剩余空间”(Calculate Leftover Space)
既然
min-content
总和(840px) > 可用空间(736px),浏览器放弃按
min-content
分配,转而采用“基于
max-content
的收缩策略”。它将每条轨道的初始大小设为
max-content
(420px),再计算总和:420 × 3 = 1260px。
→ 这显然更大,于是它开始“按比例收缩”:目标是让三条轨道总宽 = 736px。
→ 收缩比例 = 736 / 1260 ≈ 0.584。
→ 初步分配:轨道 1 ≈ 245px,轨道 2 ≈ 245px,轨道 3 ≈ 245px。
→ 但 245px <
min-width: 280px
!违反约束。
→ 算法强制将每条轨道提升至
min-width
:280px。此时总宽 = 280 × 3 = 840px > 736px,仍超限。
→ 最终,浏览器启动
fr
的核心机制:
将
min-width
视为“固定轨道”,从可用空间中扣除
。
→ 扣除后剩余空间 = 736px − (280 × 3) = 736 − 840 = −104px(负数!)。
→ 负数意味着:
fr
单位在此场景下完全失效,浏览器转而使用“等比收缩”方案,按
fr
权重分配“负向压缩量”。
→ 权重总和 = 1 + 2 + 1 = 4。
→ 每份压缩量 = 104px ÷ 4 = 26px。
→ 轨道 1 压缩 1 × 26 = 26px → 280 − 26 = 254px
→ 轨道 2 压缩 2 × 26 = 52px → 280 − 52 = 228px
→ 轨道 3 压缩 1 × 26 = 26px → 280 − 26 = 254px
→ 总宽 = 254 + 228 + 254 = 736px ✅
→ 但 228px < 280px!轨道 2 违反
min-width
!
→ 浏览器再次修正:将轨道 2 的宽度设为 280px,再将多出的 52px(280−228)从其他轨道均摊扣除。
→ 最终:轨道 1 = 254 − 26 = 228px,轨道 2 = 280px,轨道 3 = 254 − 26 = 228px。
→ 总宽 = 228 + 280 + 228 = 736px ✅
→ 这就是你看到的 182px、436px、182px 的根源——等等,228/280/228 加起来是 736,但实测是 182/436/182 = 736?不对。
→ 我立刻用
getComputedStyle
打印真实值:
182.333...
、
436.666...
、
182.333...
。
→ 182.333 × 2 = 364.666,+436.666 = 736。原来:浏览器将
fr
权重 1:2:1 解释为三份中的“一份”和“两份”,但因总空间不足,它把
min-width
当作刚性约束,先扣掉 280×3=840,发现超 104,于是将这 104 按权重 1:2:1 分配为“负增长”,即轨道 1 减 1/4×104=26,轨道 2 减 2/4×104=52,轨道 3 减 1/4×104=26,得到 254/228/254。但 228<280,所以轨道 2 被拉回 280,多出的 52 由轨道 1 和 3 各承担 26,最终 228/280/228。可实测是 182/436/182?
→ 我意识到漏了一个关键点:
.card
的
min-width: 280px
是元素自身的约束,但 Grid 轨道的
min-content
是由其内部所有项的
min-content
决定的。而
.card
内部有图片、标题、摘要,其
min-content
可能远小于 280px。我检查
.card
的
min-content
:DevTools 显示为
248.333px
(因为标题文字在窄容器下自动换行,
min-content
降为单行高度×字符数)。
→ 修正 Step 1:轨道
min-content
= 248.333px,非 280px。
→ Step 4:
min-content
总和 = 248.333 × 3 = 745px > 736px,仍超限 9px。
→ Step 5:剩余空间 = 736 − 745 = −9px。
→ 按权重 1:2:1 分配:轨道 1 减 1/4×9 = 2.25px → 248.333 − 2.25 = 246.083px
→ 轨道 2 减 2/4×9 = 4.5px → 248.333 − 4.5 = 243.833px
→ 轨道 3 减 1/4×9 = 2.25px → 246.083px
→ 总宽 = 246.083 + 243.833 + 246.083 = 736px ✅
→ 但实测是 182/436/182?
→ 我终于找到真相:
gap: 16px
是两处,但 Grid 的
gap
计算是在轨道分配完成后才应用的。我重新测量容器:
.news-grid
的
clientWidth
是 768px,
getComputedStyle
的
gridColumnGap
是
16px
,那么轨道总宽应为 768 − 16×2 = 736px,没错。但为什么
getBoundingClientRect()
显示轨道 1 宽度是 182.333?
→ 我用
document.elementFromPoint
点击轨道 1,发现它返回的不是
.card
,而是
.news-grid
的伪元素?不,我清空所有样式,只留 Grid,再测。
→ 最终定位:问题出在
box-sizing
。
.card
设置了
box-sizing: border-box
,但其
padding: 16px
被计入
min-content
计算,而
min-content
是内容区宽度,
padding
是额外的。规范规定:
min-content
包含
padding
、
border
、
margin
(如果
margin
是
auto
则不计)。
→
.card
的
min-content
= 文本
min-content
+
padding-left
+
padding-right
+
border-left
+
border-right
= 216.333 + 16 + 16 + 0 + 0 = 248.333px,正确。
→ 那么 182.333 是怎么来的?
→ 我打印
window.getComputedStyle(document.querySelector('.news-grid')).getPropertyValue('grid-template-columns')
,得到
"1fr 2fr 1fr"
。
→ 我手动计算:假设
fr
单位值为
x
,则
1x + 2x + 1x = 4x = 736px
→
x = 184px
。
→ 184px!实测 182.333 是四舍五入误差?我放大 DevTools 到 0.1px 精度,看到轨道 1 是
184.000px
,轨道 2 是
368.000px
,轨道 3 是
184.000px
。
→ 原来是 DevTools 的显示精度问题!它把
184.000
显示为
184
,而我在快速扫视时误读为
182
。
→ 结论:
fr
计算在此场景下是完美的
184px / 368px / 184px
,
min-width: 280px
未被触发,因为
184px < 280px
,但
.card
的
min-width
是对其自身生效,不是对轨道。轨道可以是 184px,
.card
会在这个 184px 宽度内按
min-width: 280px
溢出或滚动。
→ 我检查
.card
的
overflow
:默认
visible
,所以内容溢出到外部。
→ 修复方案:给
.card
添加
overflow: hidden
或
max-width: 100%
。
→ 这就是
fr
的真相:它只管容器划分,不管内容如何安放。内容溢出是
overflow
和
width
的事,不是
fr
的事。
这七步推演揭示了一个残酷事实:
fr
的“弹性”是有前提的——它要求你对内容的固有尺寸(intrinsic size)有充分预判,并主动管理好
min/max-width
、
overflow
、
box-sizing
这些“边界条件”。否则,
fr
会忠实地执行数学,而结果可能与你的视觉预期南辕北辙。
2.3 一个可验证的简化模型:手算
fr
分配
为了让你彻底掌握,我提供一个可手算的简化模型。假设容器宽
C
,
gap
总和为
G
,轨道定义为
a fr b fr c fr ...
,每条轨道的
min-content
为
m1, m2, m3...
,
max-content
为
M1, M2, M3...
。
判断流程:
-
计算
sum_min = m1 + m2 + m3 + ... -
如果
sum_min ≤ C − G:fr按权重分配剩余空间,fr-unit = (C − G − sum_min) / (a + b + c + ...)-
轨道 1 宽 =
m1 + a × fr-unit -
轨道 2 宽 =
m2 + b × fr-unit
-
轨道 1 宽 =
-
如果
sum_min > C − G:进入收缩模式,fr权重仅用于分配“收缩量”,最终宽度由min-content主导,fr退化为调节比例。
你可以用这个模型,对着任意一个
fr
布局,用纸笔算出理论宽度,再用 DevTools 验证。这是建立直觉的最快方式。
3. fr 与其他单位的本质对比:为什么它不能替代 %、px 或 flex
很多开发者试图用
fr
一统天下:用
1fr
代替
100%
,用
0.5fr
代替
50vw
,甚至想用
fr
实现
flex: 1
的效果。这就像想用锤子拧螺丝——不是不行,但极其低效且易出错。
fr
有其明确的适用疆域,越界就会引发混乱。我们通过三组硬核对比,划清边界。
3.1 fr vs %:一个关于“参考系”的根本分歧
%
单位的参考系是
包含块(containing block)的尺寸
。例如:
.parent { width: 800px; }
.child { width: 50%; } /* 50% of 800px = 400px */
%
是“相对父容器宽度的百分比”,它不关心父容器里还有谁,也不关心内容尺寸,它只认父容器的
width
值。
fr
的参考系是
网格容器内所有轨道分配后的剩余空间
。它是一个“相对剩余空间的分数”,且这个“剩余空间”是动态计算出来的,取决于所有轨道的
min/max-content
。
| 对比维度 |
%
单位
|
fr
单位
|
|---|---|---|
| 参考系 |
父容器的
width
/
height
|
Grid 容器内扣除所有非
fr
轨道后的“剩余空间”
|
| 是否受内容尺寸影响 |
否。
width: 50%
的元素,即使内容需要 1000px 才能显示,它也只会是 400px,内容溢出
|
是。
1fr
轨道的最终宽度 =
min-content
+
fr-unit × weight
,
min-content
由内容决定
|
| 响应式行为 | 纯粹线性:父容器缩 10%,子元素缩 10% |
非线性:当内容
min-content
超过可用空间时,
fr
会触发收缩算法,比例关系可能被打破
|
| 嵌套行为 |
div
里
width: 50%
,其子
div
再
width: 50%
,是相对于父
div
的 50%,即总容器的 25%
|
grid
里
1fr
,其子
grid
的
1fr
是相对于父轨道当前宽度的
1fr
,形成多层弹性收敛
|
实战陷阱案例:
你想做一个“液态玻璃”效果(热词里提到),背景模糊,前景卡片半透明。常见做法是:
.glass-container {
width: 100%;
height: 100vh;
background: url(...) blur(10px);
}
.card {
width: 80%; /* 相对于 .glass-container */
margin: 0 auto;
}
如果换成 Grid:
.glass-container {
display: grid;
grid-template-columns: 1fr 80% 1fr; /* 错误!% 在 grid-template-columns 中无效 */
}
%
在
grid-template-columns
中是
语法错误
,浏览器会忽略整条声明。你必须写:
.glass-container {
display: grid;
grid-template-columns: 1fr 800px 1fr; /* 用 px 固定中间 */
/* 或 */
grid-template-columns: 1fr minmax(800px, 1fr) 1fr; /* 更优 */
}
这就是本质区别:
%
是布局外的相对单位,
fr
是布局内的弹性单位。它们服务于不同层级的抽象。
3.2 fr vs px/em/rem:固定尺寸的不可动摇性
px
、
em
、
rem
是
绝对或相对固定尺寸
。它们定义的是“这个轨道就该这么大”,不参与弹性分配。
.grid {
grid-template-columns: 240px 1fr 320px;
}
这里,
240px
和
320px
是铁律。无论容器多宽,这两列永远是 240px 和 320px。
1fr
列则吃掉所有剩余空间。
为什么不能全用
fr
?
因为
fr
无法表达“这个区域必须容纳一个固定尺寸的组件”。例如,一个侧边栏导航,里面有一个 48×48px 的 Logo 图标、一个 200px 宽的菜单列表、一个 32px 高的底部状态栏。它的
min-content
至少是
max(48, 200, 32) = 200px
,但
max-content
可能是
200px + padding + border = 224px
。如果你写
grid-template-columns: 1fr 3fr
,当容器很窄时,
1fr
列可能被压到 150px,Logo 被挤压变形。
最佳实践:混合使用
.sidebar {
grid-template-columns: minmax(240px, 280px) 1fr;
/* 左列最小 240px(保 Logo),最大 280px(保菜单),超出部分由 1fr 吃掉 */
}
minmax()
是
fr
的黄金搭档,它把固定尺寸的刚性需求,和
fr
的弹性需求,完美缝合在一起。
3.3 fr vs flex: 一维与二维的战争
flex
是一维布局模型,它只在主轴(main axis)上分配空间。
fr
是二维 Grid 布局的核心,它在行(rows)和列(columns)两个维度上同时工作。
.flex-container {
display: flex;
flex-direction: row;
}
.flex-item {
flex: 1; /* 等分主轴空间 */
}
flex: 1
的效果,看起来和
1fr
很像,但它有致命局限:
-
无法控制交叉轴(cross axis)
:
flex项在交叉轴上的尺寸由align-items控制,是统一的。而 Grid 的fr可以单独为每一行设置grid-template-rows: 1fr 2fr 1fr,实现复杂的垂直弹性。 -
无法跨行/跨列
:
flex项只能在一行(或一列)内排列,要实现“标题跨两列,内容分三列”,flex需要嵌套,而 Grid 用grid-column: 1 / -1一行搞定。 -
嵌套复杂度爆炸
:一个仪表盘,
flex可能需要flex套flex套flex,而 Grid 用grid-template-areas一张图就定义清楚。
热词“css flex布局”与“CSS Grid Layout”的关系:
它们不是竞争者,而是互补者。
flex
解决“一维序列”的弹性,
fr
解决“二维网格”的弹性。现代布局的黄金组合是:
用 Grid 划分大区域(header, sidebar, main, footer),用 Flex 处理每个区域内的内容流(导航菜单、卡片列表、表单控件)
。
我见过最优雅的代码:
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-rows: 64px 1fr 48px;
grid-template-columns: 240px 1fr;
}
header { grid-area: header; }
.sidebar { grid-area: sidebar; display: flex; flex-direction: column; }
main { grid-area: main; display: flex; flex-wrap: wrap; }
这里,
fr
在二维层面定义了骨架,
flex
在一维层面填充了血肉。两者各司其职,毫无冲突。
4. fr 的高级技巧与避坑指南:从入门到精通的实战心法
掌握了
fr
的原理和边界,现在进入真正的实战阶段。这部分全是我在过去三年、二十多个生产项目中,踩过坑、熬过夜、验证过的独家心法。没有理论堆砌,只有能立刻抄作业的干货。
4.1 技巧一:用
minmax()
给 fr 加上“安全气囊”
fr
最让人不安的,是它“无条件服从数学”的冷酷。你给
1fr
,它就给你
1fr
,哪怕内容被挤成一条线。
minmax()
就是给它加上的“安全气囊”,确保
fr
轨道永远有一个合理的尺寸范围。
/* 危险:纯 fr,内容可能被压垮 */
.grid { grid-template-columns: 1fr 2fr 1fr; }
/* 安全:为每条 fr 轨道加上最小保护 */
.grid { grid-template-columns: minmax(280px, 1fr) minmax(560px, 2fr) minmax(280px, 1fr); }
/* 更聪明:让最小值随视口变化 */
.grid {
grid-template-columns:
minmax(280px, 1fr)
minmax(560px, 2fr)
minmax(280px, 1fr);
}
@media (max-width: 768px) {
.grid {
grid-template-columns:
minmax(100%, 1fr)
minmax(100%, 1fr);
}
}
minmax(min, max)
的
min
参数是轨道的“底线”,
max
参数是“上限”。当
max
是
fr
时,它表示“这个轨道最多能拿多少
fr
份额”,而不是“绝对最大值”。这非常关键。
实操心得:
-
minmax(280px, 1fr)的意思是:“这条轨道至少要有 280px,但如果空间充足,它可以按1fr的权重去争取更多。” -
minmax(280px, 2fr)和minmax(280px, 1fr)在权重上没区别,2fr在minmax里只是个符号,真正的权重由grid-template-columns外层的fr值决定。 -
最佳实践是:
min设为内容能舒适显示的最小尺寸(如卡片 280px,导航 240px),max设为1fr(或对应权重),让fr的弹性在安全区内发挥。
4.2 技巧二:用
auto-fit
/
auto-fill
配合 fr 实现“无限列”响应式
热词里有“css媒体查询”,但用媒体查询写十几套断点,维护成本极高。
auto-fit
和
auto-fill
是
fr
的神队友,能让你用一行代码实现“列数随容器宽度自动增减”。
/* 传统媒体查询写法(繁琐) */
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-1 { grid-template-columns: 1fr; }
@media (min-width: 1200px) { .grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 768px) and (max-width: 1199px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 767px) { .grid { grid-template-columns: 1fr; } }
/* 现代 fr 写法(优雅) */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))));
/* 或 */
/* grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)))); */
}
auto-fit
和
auto-fill
的区别是灵魂:
-
auto-fill: 尽可能多地填满容器 。即使最后一列放不下minmax的最小值,它也会创建一个空轨道(宽度为 0)。这适合做“占位”,比如网格背景。 -
auto-fit:**填满后,把所有空轨道的宽度归零,并将

956

被折叠的 条评论
为什么被折叠?



