排版溢出怎么查:从盒模型到最小宽度
卷中目录
先区分绘制出去和能够滚到
页面右侧多出一条横向滚动条,卡片标题压进邻列,按钮被切掉半个字,粘性页眉盖住下面的正文,这些现象都常被叫作溢出,查法却不是同一条路径。阴影、轮廓、下划线、滤镜可以画出边框盒之外,这是墨水溢出,它不一定让任何滚动容器变宽。另一类是滚动溢出:某个滚动容器的 scrollWidth 大于 clientWidth,或者视口本身能横向滚动,用户能把那一截内容滚进视野。排查时先问一句:多出来的像素能不能被滚动到。能滚动,就沿滚动容器从外向内找超宽的后代;不能滚动、只是画在外面,就看阴影、轮廓、负外边距和变换,不要在宽度属性上打转。

只给祖先加上 overflow: hidden 会让滚动条消失,也会把焦点环和盒阴影一起切掉。症状被藏起来之后,下一次内容稍长,裁切会发生在更难看见的那一层。隐藏是裁切手段,不是定位手段。
盒模型上多出来的那一圈
宽度怎么加,先看 box-sizing。content-box 下,写在 width 上的长度只约束内容盒,左右内边距和左右边框还要往外加。一栏已经占满父级的内容盒,再加两侧内边距和边框,父级就会被撑出。border-box 把内边距和边框算进设定宽度,这种加法溢出会少很多。但它不能把内容压到比最小内容尺寸还窄:内容自身不肯断行时,边框盒仍会被内容顶开,多出来的部分变成溢出。
水平外边距不折叠。子元素的左右外边距会直接加进父级所需的空间。负外边距把元素拉出容器时,看起来像溢出,测量边框盒却发现它自己的宽度并没有变大。轮廓和 box-shadow 不参与布局,父级若不是 overflow: visible,阴影会被切掉,这经常被误认为栏宽不够。对齐设计稿时要看盒模型叠层,而不是只读计算后的 width 一个数字。稿上的栏宽通常是边框盒,标注的内边距在盒子内部。用 content-box 又把标注宽度原样写进 width,实现就会比稿宽出一圈内边距。
滚动条自己也占宽度。纵向内容变高之后出现经典滚动条,内容区的内宽变小,原来刚好放下的单行标题就开始横向溢出。scrollbar-gutter: stable 可以让这条占位始终留着,有无滚动条时内宽一致。视口宽度单位 vw 按视口计算,不扣除经典滚动条。用它去对齐整页时,纵向滚动条一旦出现,横向就会多出大约一条滚动条的宽度。更稳的参照是包含块的内宽,或使用 stretch 这一类与可用空间相关的尺寸,而不是用视口单位去硬贴视口。
自动最小尺寸怎样从内容传到轨道
伸缩项的常见初始值是 flex: 0 1 auto,同时 min-width 为 auto。对伸缩项而言,auto 会解析成自动最小尺寸:通常不会小于该项内容的最小贡献,再与明确指定的尺寸、最大尺寸相互钳制。一串不换行的订单号、一张固有宽度很大的图、一个 white-space: nowrap 的标签,都会把这条最小宽度抬高。父级再窄,子项也拒绝缩小,溢出从这一项开始,再传到外层滚动容器。
网格有同一条默认行为。1fr 是 minmax(auto, 1fr) 的简写,轨道最小尺寸是自动,常常等于网格项里那份最大的最小内容贡献。只改份数、不改最小值,窄容器里轨道仍然不肯收缩。要让列真的让出空间,最小值应写成 0,例如 minmax(0, 1fr)。改完之后溢出若从轨道挪进了单元格内部,说明外层已经肯缩,接下来该处理单元格里的换行或内部滚动,而不是再把网格的最小值改回去。
自动最小尺寸会沿布局树向外传。内层一个不换行的标签,可以逐级抬高卡片、抬高列、抬高页面。查的时候不要只盯着出现滚动条的那个祖先,它往往只是被里面某只盒子顶开的结果。
哪些东西会变成不可分割的宽度
最小内容尺寸,可以理解成:在不把盒子撑破的前提下,把它压到尽量窄时,内部不可分割片段所需要的宽度。常见的不可分割片段有这些:
连续的英文、数字和产品型号,默认只在空白处断行。一长串没有空格的路径、完整 URL、Base64,会把最小内容宽抬到整串的长度。white-space 取 pre 或 nowrap 时,换行被关掉,整段都变成不可分割片段。pre-line 仍保留换行符,但长词本身照样不断。
图片、视频、画布和内嵌框架带有固有尺寸。只给图片写最大宽度,而它所在的伸缩项仍按固有宽度计算最小内容,约束还没有着力点。要在图片上限制相对于容器的最大宽度,并让它所在的项允许缩到 0,高度再按比例跟着走。没有 viewBox、只写死宽高的 SVG,缩放行为和带 viewBox 的图不一样,需要单独看,不能沿用同一句样式。
输入框和选择框有浏览器给的默认尺寸。表格在自动布局下,单元格的最小内容会参与列宽,一个不换行的长词能把整张表推出容器。表格若必须留在窄栏里,让表格的容器承担横向滚动,不要让滚动条冒到整页上。表头若要粘住,粘性定位的滚动祖先必须是这同一个容器,否则表头相对错误的盒子定位,看起来又像叠层错位。
中文正文默认可以在字与字之间换行,纯中文段落很少单独造成横向溢出。混排时,包在行内盒子里的型号、英文用户名、不换行的价格,会在段落中间撑出一条窄的横条。检查时要选中行内元素看,不要只量整段的边框盒。
一条可以重复的检查顺序
把窗口缩到能复现问题的宽度,记下视口宽度,以及真正出现滚动条的那个容器。在宽屏上猜测,很容易改到一条在窄宽度下根本不生效的规则。
选中文档的滚动元素,比较 scrollWidth 和 clientWidth。若文档本身没有多出来的宽度,就不要继续在 body 上加约束。问题要么在某个内部滚动区,要么只是墨水画到了边界外,要么来自固定定位元素。固定定位相对于视口,超宽的固定层会让视口出现横向滚动,却不增加某个祖先的 scrollWidth。这种时候单独点选 position: fixed 的元素,看它的边缘是否超出视口,并检查宽度与左右偏移相加之后的和。
从可疑容器往下走,读计算后的 width、min-width、max-width、box-sizing、flex-basis、flex-shrink,以及网格轨道的 minmax。计算样式里 min-width 若仍是 auto,再看它解析后的像素。像素若贴近内容宽度而不是容器宽度,自动最小值就在起作用。
临时给后代加轮廓,看是哪一只盒子比容器宽。这比在样式表里到处改 overflow 更不容易把原因盖住。找到那只盒子之后,分清宽度来自内容最小尺寸、显式长度、外边距,还是视口单位。来源不同,改法不同。
若它是伸缩项或网格项,先只把该项的最小宽度改为 0,确认溢出是否因此收进该项内部。文字随后在项内溢出,再决定换行、截断或项内滚动。三件事一次改完,就无法知道是哪一条真正生效。
换行策略要按片段选择
普通句子可以用 overflow-wrap: break-word:一行放得下时仍在正常断点换行,放不下才在词内断开。若希望长词参与最小内容的计算、免得它先把父项撑开,用 overflow-wrap: anywhere,它会影响最小内容尺寸。word-break: break-all 对英文会切得很碎,中英混排里通常过重。中文标点的禁则交给 line-break,不要为了消掉滚动条把所有语言都切成单字。
截断必须另有路径看见全文,例如本来就存在的详情,或一处可到达的展开。只有 text-overflow 而没有全文,信息就丢了。text-overflow 生效还需要该项宽度被限制、横向溢出被隐藏、空白不换行,三者缺一个,省略号就不会出现,页面上只剩溢出或只剩裁切。
内部滚动要能用键盘到达。焦点进到滚出视野的控件时,滚动容器应跟着把焦点滚进可见区域。只做鼠标拖动的横滑,键盘用户会停在裁切边缘之外。
最小宽度不单写在 min-width 上
这些设置都会抬高或压低有效的最小宽度。flex-basis 或 width 是固定长度,并且 flex-shrink 为 0 时,项根本不被允许缩小。min-width 写成设计稿上的某个长度时,它比自动最小值更硬,容器再窄也要遵守。内边距在 border-box 下挤占内容区;内容不能换行时,最小尺寸至少是左右内边距加上不可断行片段。边框变粗也会挤窄内容区,单行文本因此开始溢出。
字号和字体同样参与。回退字体比主字体宽时,一行的宽度会在字体加载完成后突然增加,页面先正常、后溢出。这既是布局偏移,也是溢出。给回退字体准备接近的度量,避免加载完成后再把行宽撑开。letter-spacing 与 nowrap 合在一起,疏排会把最小内容宽大致按字符数加大。
aspect-ratio 不会取消最小内容贡献。一张图同时有比例和固有尺寸时,在伸缩布局里仍可能按固有宽度抬高最小尺寸,比例只决定高度如何跟着宽度走。输入框若使用按内容收缩的字段尺寸特性,占位符很长时,最小宽度会跟着占位符变,空表单比填完的表单更容易把栏撑破。
一像素级别的溢出常来自亚像素取整。几列按份数平分,在 1.25 或 1.5 的缩放下四舍五入,总和可能比容器多出一像素,浏览器之间取整方向还不一致。只在特定缩放下出现横向滚动时,优先怀疑取整,而不是内容突然变长。允许轨道收缩,比写死扣掉一像素更不容易在下一种缩放下重新露出。
包含、容器查询和书写方向
组件设了 container-type: inline-size 之后,它成为尺寸容器。内部布局可以查询容器的内联尺寸,容器自身的尺寸却不能再依赖那些会造成循环的内容贡献。加上容器查询后,卡片可能不再被内部内容撑开,也可能反过来在外层溢出。两种方向都要同时看容器的宽度是谁给的,不要只改查询条件里的断点。
contain: inline-size 会切断内部最小内容向外传递的路。它可以用来阻止一张宽表撑开整页,前提是另外给这个容器一个确定的宽度。没有确定宽度时,包含和自动宽度叠在一起,结果难以预测。overflow: clip 与 hidden 也不同:clip 裁切且不建立滚动容器,程序化滚动不会作用在它身上,焦点是否能把被裁切的控件带进视野,需要单独验证。
界面要支持从右到左的书写方向时,溢出不一定出现在右侧。用物理方向的 left、right 写死的补丁,在 rtl 下会补到相反的一侧。把 dir 临时改成 rtl,看滚动条出现在哪一侧,粘性元素贴的是哪一条边。逻辑属性里,min-inline-size 才是书写方向上的那条最小尺寸。竖排 writing-mode 下,口头说的横向可能已经变成块向。一段不换行的文本会增加块向尺寸,症状从横向滚动变成纵向滚动,检查的轴要跟着换。
变换不改变布局是否溢出
变换之后,元素在布局上仍占变换前的盒子。视觉上移出容器时,scrollWidth 可能不变。反过来,布局已经溢出的元素再用变换移回视野,滚动条仍然在。排查时把变换临时关掉,确认多余宽度来自布局计算,而不是绘制位置。滤镜和阴影同理,它们不改变布局盒。用变换做入场动画时,不要用远远移出屏幕的位移去制造效果,那会在动画关闭或被减少时留下一个本来就超宽的布局盒。动效若要移动元素,移动的是绘制,宽度约束仍然要在静态布局里先成立。
一个会反复出现的卡片
卡片是纵向伸缩容器,上面是图,下面是文字。外层网格的列写成 1fr。图片来自接口,固有尺寸很宽。文本区有一条订单号,样式是不换行。窗口较宽、三列并排时看起来正常,因为列宽仍大于订单号和图片的最小贡献。窗口收到大约一列的窄宽度时,轨道的自动最小值不肯低于这些贡献,整页出现横向滚动。
改法要留在每一层。网格轨道改成 minmax(0, 1fr),列可以变窄。图片限制相对于容器的最大宽度,所在的项允许缩小,高度按比例变化。订单号允许在任意位置断开,或者给它所在的区域单独横向滚动。只改其中一处,另外两处会在更窄的窗口或更大的字号下重新撑开。把字号放到页面设置里偏大的一档再看一次,因为同一条订单号的像素宽度变了,开始溢出的窗口宽度也会移动。
空状态和满状态都要量。只有一个短词的标签,和一排很长的标签,最小宽度完全不同。溢出常常只在真实数据里出现:用户名是一整段邮箱,标题里夹着连续型号,评论里贴了一行没有空格的代码。用设计稿上的几个短字验收,这些情况会全部漏过。
把结论写成可以复用的三句话
每次查清之后记下三句。哪一个元素的最小宽度来自哪一段内容。哪一条布局约束把这个最小宽度传出了容器。最后选择的是换行、内部滚动,还是允许该项缩到 0。这三句比一张修完后的截图有用。下一次同类症状,可以对上号,而不是从盒模型重新猜。
组件验收里可以固定这几项:窄视口、放大字号、一长串不换行文本、一张超宽图、从右到左的书写方向、出现纵向滚动条之后再看横向。溢出不是随机的样式事故,它是最小宽度沿布局树向外传递的结果。顺着传递路径看,能找到该改的那一层,而不是在最外层把滚动条藏掉。
见字如晤