10 月 8 日给博客的搜索弹层加了两组建议:热门标签和最近 5 篇文章,打开搜索不用输入就能直接点。我在 1440 宽的电脑上截了图,排得整整齐齐,就发版了。
没过多久收到一张手机截图:标签那一行最右边的「面试」只露出半个字,最近文章的标题直接伸出了弹层的右边框,「后台的『分享签名校验』」整段被切掉。
问题不难修,难堪的是它本来一眼就能看到——我根本没在手机尺寸下打开过这个弹层。这篇把这次踩到的三个坑记下来,最后附上之后用来量这类问题的办法。
现象:电脑上正常,手机上越界
先用脚本量一遍线上版本:打开弹层,数一数弹层里有多少个元素的边框超出了弹层本身。
| 视口 | 弹层宽 | 越界元素 |
|---|---|---|
| 360×780 | 320px | 20 个 |
| 320×568 | 280px | 20 个 |
| 390×664 | 350px | 19 个 |
| 1440×900 | 580px | 0 个 |
电脑上一个都没有,手机上全军覆没。最长的那篇标题单行排开约 518px:比电脑上的 580px 弹层窄,比手机上的任何一个都宽。所以这个问题在电脑上根本不会出现。
一、网格的 auto 列,不会比内容窄
建议区是这样写的:
.search-suggest { display: grid; gap: 14px; }
.search-suggest-posts { display: grid; gap: 2px; }
.search-suggest-posts a {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
本意是标题太长就在末尾显示省略号。可省略号一个都没出现,标题反而把整块撑了出去。
原因在 display: grid 没写 grid-template-columns。这时网格只有一条隐式的列,宽度是 auto。auto 列有个下限:不能比里面内容的「最小内容宽度」更窄。而一段 nowrap 的文字不能在任何地方断开,它的最小内容宽度就是整行的长度。
于是标题有多长,这一列就有多宽。链接自己的 overflow: hidden 根本没机会生效:它拿到的宽度本来就放得下整行,没有什么可截的。
用一个 300px 宽的容器单独复现,量到的数字是这样的:
| 写法 | 列宽 | 链接宽 |
|---|---|---|
| 默认 auto 列 | 518px | 518px |
| 只改外层 | 300px | 518px |
| 两层都改 | 300px | 300px |
第二行是我修第一遍时踩的:外层网格改成 minmax(0, 1fr) 之后,外层的列确实缩回了 300px。可最近文章的列表 ul 自己也是一个网格,它的列还是 auto,链接照样是 518px。线上量出来的越界元素从 20 个降到 10 个,没有归零。
两层都改过之后,列宽回到 300px,省略号也出来了:
.search-suggest {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.search-suggest-posts {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.search-suggest-posts li { min-width: 0; }
minmax(0, 1fr) 的意思是:这一列最小可以是 0,最大占满剩余空间。下限从「内容的最小宽度」改成了 0,列就会老老实实服从容器的宽度。
标签那一行被切掉也是同一个原因。标签是 flex-wrap: wrap,按容器宽度换行,可它的容器已经被标题撑到了 518px,标签就排到 518px 才换行。手机上看,就是最右边那个标签被弹层的边框切掉。
flex 布局里也有同样的规则:flex 子项默认 min-width: auto,同样不会比内容窄。碰到不换行的长文字、长网址、长代码,要么给子项写 min-width: 0,要么给列写 minmax(0, 1fr)。
二、两行截断,漏出了第三行的半截
列宽对了,省略号也出来了,可手机上 5 篇标题被截掉 4 篇,每篇只剩一行,长标题的后半句全看不到。
| 视口 | 单行省略 | 两行截断 |
|---|---|---|
| 360×780 | 截 4 篇 | 截 1 篇 |
| 320×568 | 截 5 篇 | 截 2 篇 |
| 390×664 | 截 4 篇 | 截 0 篇 |
于是改成最多显示两行:
.search-suggest-posts a {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding: 7px 10px;
}
截图一看,两行之下还能看到第三行文字的上半截,像被人用尺子沿着字腰划了一刀。(站里另一篇《基于 -webkit-line-clamp 的评论折叠组件开发》写过怎么用这个属性做折叠,这次碰到的是它和内边距之间的关系。)
-webkit-line-clamp 只负责在第二行末尾加省略号,后面的文字照样排版,只是被 overflow: hidden 裁掉。而 overflow 是按内边距的外缘裁的:内边距本身也算可见区域。
按 20px 行高算:
- 两行文字占 40px,上下内边距各 7px,元素的可见高度是 54px;
- 第三行从第 47px(7 + 40)开始,到第 54px 被裁掉,正好露出 7px,差不多是半个字。
量到的也是这样:可见高度 54px,内容总高 74px。
解决办法是把上下的留白从内边距换成透明边框:
.search-suggest-posts a {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding: 0 10px;
border: 7px solid transparent;
border-left: 0;
border-right: 0;
}
边框不属于裁切区域,可见高度变成正好 40px,第三行一个像素都不露。背景默认会铺到边框底下,所以鼠标悬停时那块浅色背景的大小和原来一样。
三、固定的弹层,底边出了屏幕
两行截断让每篇标题高了一截,在 iPhone SE 这种 320×568 的小屏上,弹层从第 102px 开始,到第 681px 才结束,比屏幕底部多出 113px。
如果只是长一点,往下滑就行。可这个弹层是固定定位的,打开时页面又被锁住了滚动(body 设成了 position: fixed),整个屏幕没有任何地方能滚。最后两篇文章就在屏幕下面,怎么都点不到。
做法是让弹层自己限高,超出的部分在弹层里面滚:
.search-dialog {
display: flex;
flex-direction: column;
max-height: calc(100vh - 120px);
max-height: calc(100dvh - 120px);
}
.search-input-area { flex: none; }
.search-suggest {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
几个细节:
100dvh是去掉浏览器地址栏之后的可见高度,不认识它的旧浏览器会用上面那行100vh;- 输入框不参与收缩,保证搜索框始终在;
min-height: 0又是第一节那条规则:flex 子项默认min-height: auto,不会比内容矮,不写这一句,overflow-y: auto永远等不到需要滚动的那一天;overscroll-behavior: contain让滑到头时不会带着后面的页面一起动。
改完再量:
| 视口 | 改前底边 | 改后底边 |
|---|---|---|
| 320×568 | 681px | 550px |
| 390×400 | 594px | 352px |
390×400 模拟的是键盘弹起后只剩一半屏幕的情况。
最后用触摸手势在建议区里往上划了 300px:建议区滚动了 130px,最后一篇完整露出来,背后的页面一动没动。
之后怎么测
这次的根本问题不在 CSS,而在于没测。之后凡是改了弹层、网格、不换行文字、固定定位的地方,发版前都按这个办法在几个尺寸下各量一遍:
// 打开弹层之后在页面里执行:弹层里有哪些元素越过了弹层的左右边界
const dialog = document.querySelector('#search-overlay .search-dialog');
const box = dialog.getBoundingClientRect();
const outside = [...dialog.querySelectorAll('*')].filter(function (el) {
const r = el.getBoundingClientRect();
return r.width > 0 && (r.right > box.right + 0.5 || r.left < box.left - 0.5);
});
const result = {
outside: outside.length,
pageOverflow: document.documentElement.scrollWidth > innerWidth,
bottomOut: box.bottom > innerHeight
};
- 尺寸至少覆盖 360×780、320×568、390×664 三个手机尺寸和一个电脑尺寸,浅色和深色各一遍;
- 三个数都要是 0、
false、false,再截图人工看一眼; - 要滚动的地方用触摸手势真的划一下(Chrome 开发者协议里的
Input.synthesizeScrollGesture),看滚的是不是该滚的那个元素。
小结
- 网格的 auto 列和 flex 子项默认都不会比内容的最小宽度更窄,碰到不换行的长文字,写
minmax(0, 1fr)或min-width: 0;嵌套的每一层都要检查。 overflow: hidden按内边距的外缘裁切,多行截断要留白时用透明边框,别用上下内边距。- 固定定位的弹层要自己限高、自己滚,
flex子项记得写min-height: 0。 - 「在电脑上看过」不等于「测过」。

评论区 0