给首屏写 min-height: 100vh,桌面端刚好一屏,手机上一打开就多出一条滚动,或者底部那块内容被地址栏压在下面。
这不是 bug。移动浏览器里的 vh 指的是大视口——地址栏折叠起来时的高度,也就是这个屏幕能给出的最大高度。而地址栏通常是展开的,于是 100vh 永远比你此刻能看见的区域高那么一截(几十像素,取决于设备)。
现在有四套视口单位
| 单位 | 含义 |
|---|---|
lvh |
large viewport —— 地址栏折叠时的最大高度,就是以前的 vh |
svh |
small viewport —— 地址栏展开、UI 最占地方时的最小高度 |
dvh |
dynamic viewport —— 跟着地址栏状态实时变化 |
vh |
等于 lvh,为兼容保留 |
落地情况:Chrome 108、Safari 15.4、Firefox 101 起支持。再往前的浏览器只认 vh。
所以降级写法不需要任何 JS,两条属性叠着写就行:
.hero {
min-height: 100vh; /* 不认 dvh 的浏览器用这条 */
min-height: 100dvh; /* 认的话,这条覆盖上一条 */
}
CSS 的机制是”不认识的值直接丢弃这条声明”,后面的照常生效。所以这种写法是安全的,不是 hack。
dvh 的副作用:会跳
dvh 跟着地址栏变。用户上下滑动、地址栏折叠展开,100dvh 的高度就在几十像素之间来回变——如果里面有个居中的大标题,它会跟着跳。
我的用法是分开:
- 首屏 hero 用
dvh:它就该刚好一屏,跳一下也比被切掉好。 - 滚动区域内部不要用视口单位,用
flex: 1或者百分比——父容器的高度已经确定,不需要再问一次视口。 - 确实要”至少一屏但不能跳”的地方,用
svh:它取最小值,永远保证内容不被地址栏挡住,同时不会变。
被 home indicator 挡住的那一条
iPhone 底部有一条横线(home indicator),固定定位的底栏如果贴到最底,会被它压住。这需要两样配合:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.bottom-bar {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
viewport-fit=cover 让页面允许延伸到安全区之外(否则浏览器本来就会给你留边),env() 再把内容推回安全区内。0px 那个默认值是给不支持的浏览器兜底的。
这套在横屏刘海手机上尤其重要——左右两侧也有 safe-area-inset-left/right。
width=device-width 到底改了什么
这一条是响应式能生效的前提,但它的作用经常被误解成”让页面宽度等于屏幕宽度”。更准确的说法是:它把布局视口的宽度设成设备的 CSS 像素宽度。
不写它的时候,移动浏览器默认用一个大约 980px 宽的布局视口渲染,然后把整页缩小到屏幕里——所以不写 viewport 的桌面页面在手机上是”完整的缩略图”,字小到看不清。写了之后布局视口 = 设备宽度,媒体查询才有意义。
断点该按内容定,不是按设备定
iPhone 375、iPhone Pro Max 430、平板 768——按这些数字设断点,你会发现每年新机型都要补一段。更稳的做法是把浏览器窗口慢慢拖窄,看哪一行开始挤、哪个标题开始换行难看,那个宽度就是你的断点。
设备尺寸会变,内容的舒适宽度不会。我这个站的卡片是三列,因为 900px 以下三列时摘要只剩两行半,读起来太挤——900 这个数字来自内容,不是来自任何一款手机。