100vh在移动端不可靠,应改用top/bottom撑开fixed元素,并用@supports(100dvh)降级配合min-height:100vh,同时重置box-sizing和适配safe-area。

因为 100vh 在移动端不是按你此刻看到的屏幕高度算的,而是按“地址栏收起时的最大可能高度”计算,fixed 元素一旦依赖它,就容易撑出视口、遮挡内容或触发意外滚动。
100vh 的计算逻辑在 iOS/Android 上根本不可信
刚打开页面时 Safari 地址栏展开,window.innerHeight 可能是 762px(iPhone 13),但 1vh 却被解析为 812px ÷ 100 = 8.12px —— 这意味着 100vh 实际是 812px,比你能操作的区域高了约 50px。这个偏差会直接作用在 position: fixed 元素上,导致它底部悬空、按钮被工具栏盖住,或者触发垂直滚动条。
更麻烦的是:滚动后地址栏收起,window.innerHeight 变大,但 CSS 不重计算 100vh 值,fixed 元素位置卡在旧高度,造成视觉“跳变”。
fixed 元素用 100vh 时,父容器高度不参与约束
很多人给 position: fixed 元素写 height: 100vh,却忘了 fixed 元素脱离文档流,它的尺寸不继承父容器高度。即使父级是 position: relative,只要没显式设 height 或 min-height,100vh 就只认视口,不管上下文。
立即学习“前端免费学习笔记(深入)”;
- 错误写法:
.modal { position: fixed; height: 100vh; }→ 溢出风险高 - 更稳写法:
.modal { position: fixed; top: 0; bottom: 0; }→ 高度由视口边界自然撑开,不依赖 vh 计算 - 若需 padding 或安全区适配,用
env(safe-area-inset-bottom)补在bottom上,而不是加在 height 里
@supports(100dvh) 降级必须写对顺序
100dvh 是语义正确的解法,它始终等于当前视觉视口高度,地址栏伸缩时自动响应。但它在 iOS 15 及更早、旧版 Samsung Internet、多数微信 WebView 中完全无效 —— 元素会塌成 min-height: auto,整块消失。
所以必须这样写:
body {
min-height: 100vh;
}
@supports (min-height: 100dvh) {
body {
min-height: 100dvh;
}
}注意两点:
-
min-height: 100vh必须写在@supports外层,否则老设备连降级都拿不到 - 必须用
min-height,不是height—— 否则内容超长时会被裁剪 - 全局重置盒模型:
* { box-sizing: border-box; },否则padding和border会悄悄加高 fixed 元素总高度
overflow: hidden 不是修复,只是掩盖
有人发现加了 overflow: hidden 就不滚了,于是以为问题解决了。其实这只是把溢出内容切掉了,fixed 元素依然错位,env(safe-area-inset-bottom) 可能失效,iOS 上键盘弹起时内容被顶飞、flex 居中偏移等问题全还在。
真正要盯住的,从来不是“怎么让它不滚”,而是“怎么让它的高度基准和用户此刻看到的一致”。这需要同时处理单位选择、降级顺序、盒模型、安全区四个层面 —— 漏掉任意一个,都可能在某个机型上白屏或错位。


















