原创

Grid 两列加 gap 后横向溢出:百分比与 fr 的空间预算不同

两列分别占50%,中间再留24px,为什么容器会横向溢出24px?因为百分比列已经把容器宽度分完,gap没有自动从那两个百分比里扣除。

这个问题不需要长链接、表格或不可断词才能重现。用两个很短的卡片,就可以把百分比轨道与剩余空间分配的区别看清。不要先拿overflow:hidden遮住,隐藏溢出会让截图整齐,却没有改变那份算不下的尺寸预算。

让两种预算在同一面板上切换

保存为 grid-gap.html,直接在现代浏览器打开。页面初始使用40%和60%,切换后使用2fr和3fr。比例相同,gap的参与方式不同。

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>Grid 百分比与 gap 实验</title>
<style>
body { margin: 24px; font: 16px sans-serif; }
label { display: block; margin-bottom: 12px; }
.frame { width: min(600px, 85vw); border: 2px solid #26374e; }
.grid { display: grid; grid-template-columns: 40% 60%; gap: 24px; }
.grid.fraction { grid-template-columns: 2fr 3fr; }
.card { height: 110px; background: #d9e7fa; display: grid; place-items: center; }
.card + .card { background: #f4dfb9; }
pre { white-space: pre-wrap; }
</style>
<label><input id="mode" type="checkbox">改成 2fr / 3fr</label>
<div class="frame">
  <div class="grid"><div class="card">左列</div><div class="card">右列</div></div>
</div>
<pre id="report" role="status"></pre>
<script>
const grid = document.querySelector('.grid');
const cards = [...document.querySelectorAll('.card')];
function report() {
  const width = grid.getBoundingClientRect().width;
  const tracks = cards.map(card => card.getBoundingClientRect().width);
  document.querySelector('#report').textContent = [
    `容器内容宽度:${width.toFixed(2)}px`,
    `两列宽度:${tracks.map(n => n.toFixed(2)).join(' / ')}px`,
    `两列加间隙:${(tracks[0] + tracks[1] + 24).toFixed(2)}px`,
    `clientWidth=${grid.clientWidth}, scrollWidth=${grid.scrollWidth}`
  ].join('\n');
}
document.querySelector('#mode').addEventListener('change', event => {
  grid.classList.toggle('fraction', event.target.checked);
  report();
});
window.addEventListener('resize', report);
report();
</script>
</html>

容器内容宽度为600px时,百分比列是240px和360px,加上间隙共624px。分数轨道先在相应轨道计算中考虑间隙,再按2比3分配可用空间,两列总宽加24px回到600px。浏览器可能产生小数与像素取整,页面打印的是实际布局结果。

改为2fr和3fr后,两列为间隙让出空间,滚动宽度回到600px。 改为2fr和3fr后,两列为间隙让出空间,滚动宽度回到600px。

40%与60%轨道占满600px后再增加24px间隙,实际滚动宽度为624px。 40%与60%轨道占满600px后再增加24px间隙,实际滚动宽度为624px。

百分比回答“占谁的几成”

这里轨道百分比依据Grid容器的相应尺寸计算,不是依据“扣完所有间隙以后剩余的尺寸”。两列的百分比总和为100%,只说明轨道本身已经占满;gap是额外布局间隙,还需要自己的空间。

所以换成49%与49%,也没有形成稳定修复。它只是在人为留出2%宽度后,碰巧可能容纳某个屏幕上的24px。容器窄了可能继续溢出,容器宽了又会出现多余空白,比例与固定间隙之间仍然没有建立正确关系。

如果设计明确要求“整个容器宽度的40%”作为左栏宽度,那么百分比可能就是合适的单位,只是右栏不能再无条件取60%然后增加gap。可以让右栏承担剩余空间,或使用明确的calc表达预算。选择哪一种,取决于百分比是否真的属于设计合同。

两栏应当按2比3分享扣除间隙后的空间时,分数轨道更直接。单位表达对了,调整gap时就不需要同时修改两条百分比公式。

fr 也不是所有溢出的保证书

这份实验刻意只有短文字,目的是让溢出只由百分比和gap造成。实际卡片还可能有长内容、固定宽度后代、内边距、最小尺寸等约束。换成fr之后仍溢出,应继续检查这些约束,而不是得出“fr没有用”的结论。

例如给某个卡片添加一个固定800px的子元素,当前600px容器就出现另一项不兼容预算。那已不是本例的24px间隙问题,需要决定内容是否允许换行、缩放或滚动。不要用同一个补丁解释所有横向滚动条。

Grid轨道的自动最小尺寸也会参与内容约束。需要零最小轨道时,应针对具体内容采用合适的minmax等策略;但在本文短卡片里引入这些写法,反而会掩盖我们要比较的单位差异。诊断时一次保留一个变量更有效。

读尺寸时,把边框与内容区分开

外面的frame有2px边框,Grid自身没有边框或内边距。因此报告中的Grid宽度可以直接用于这次轨道计算。若改成测frame的getBoundingClientRect,它会包含边框,不能直接把测量值当成轨道百分比参照。

clientWidth和scrollWidth则以整数CSS像素呈现。对边缘处的小数溢出,单看两个整数可能不足以定位,应该结合元素矩形和浏览器布局工具。样例的24px差异足够明显,不需要依赖一个像素的偶然取整。

响应式页面还应在窄屏检查,不能只测600px。把浏览器缩窄,百分比模式仍然多出固定24px,而分数模式随容器调整。若卡片内容开始触发最小尺寸,就把它作为另一项输入单独保留下来,区分是单位预算还是内容约束。

两个常见补丁为什么没有解决预算

给卡片加box-sizing:border-box,只是改变卡片自身宽高如何包含边框与内边距,不会让两个百分比轨道替gap让出空间。样例卡片连边框和内边距都没有,仍然溢出24px,已经足以排除那条解释。真实页面可以同时存在盒模型和轨道问题,但应分别验证。

justify-content:space-between也不是从已满的轨道里减去24px。内容分布处理的是轨道之后的相应空间,不能把100%的两条显式轨道重新定义成“扣完间隙的100%”。看到布局属性很多,先问它作用于哪一步,往往比轮流尝试所有对齐值更省时间。

如果确实选择calc扣除间隙,扣多少还与分栏比例有关。两列各写减12px,能在五五分时形成对称预算,却不是四成六成的唯一合理解释;设计若要求扣除间隙以后仍保持二比三,应按那份剩余空间分别分配。使用fr可以直接表达这项意图,减少公式与视觉比例脱节的机会。

布局实验也不要让测量脚本反过来改变宽度。此页只读取尺寸并写到独立报告区,没有依据读到的scrollWidth继续扩大容器,因此开关前后的差异来自CSS本身。复制到复杂组件时,若还存在ResizeObserver驱动的自适应逻辑,应先停掉那层反馈,再确认基础布局。

最后把gap从24改成40,并同步测量脚本里的常量。百分比模式的溢出应随之增加,分数模式的轨道会为新间隙让出空间。这个修改给出了直接的因果验证:不是某条“清浮动”样式起了作用,而是空间分配模型终于包含了间隙。

正文到此结束
评论插件初始化中...
Loading...