CSS 100vh 在手机上底部被截断
CSS 100vh 在手机上底部被截断的问题与终极解决方案
你满怀期待地给页面设置了 height: 100vh,在电脑上一切完美,可一到手机上,底部导航栏立刻被截断,或者页面下方多出一片难看的空白。这不是你的代码写错了,而是移动浏览器的视口机制埋下的“坑”。本篇教程将带你彻底搞懂原因,并提供从现代CSS到稳健兼容的全套解决方案。
1. 问题的发现:为什么 height: 100vh 不能填满屏幕?
在移动端浏览器(Safari、Chrome、Firefox 等)中,一个常见的交互是:手指向上滑动页面时,浏览器顶部的地址栏和底部工具栏会自动隐藏,为内容腾出更多空间;向下滑动时,这些 UI 又会再次出现。
当你给一个元素设置 height: 100vh 时,你期望它始终占满整个可见屏幕。但实际效果却是:
- 工具栏隐藏时:页面底部原本被工具栏遮挡的区域会暴露出来,如果页面背景不是纯色,就会看到丑陋的断层或空白。
- 工具栏出现时:元素高度可能超出实际可视区域,导致重要内容(如底部按钮、选项卡)被工具栏覆盖,无法点击。
这一切的根源在于,100vh 的计算方式在移动端浏览器中并不总是等于“用户眼睛看到的窗口高度”。
2. 深入原因:移动浏览器的地址栏动态变化与视口定义
要理解这个问题,需要区分三个概念:
- 布局视口(Layout Viewport):CSS 在计算百分比、视口单位时所参考的矩形区域。传统的
100vh获取的就是这个高度。 - 视觉视口(Visual Viewport):用户当前实际可见的网页区域,会随缩放、键盘弹出、工具栏显示/隐藏而变化。
- 初始包含块:根元素
<html>的包含块。
问题的关键:在移动端,浏览器为了获得更好的视觉连续性,动态工具栏的隐藏并不会改变布局视口的高度。当你打开页面时,布局视口已经固定为屏幕的物理高度 D(不含状态栏)。即使后来地址栏隐藏、视觉视口变大,100vh 仍然保持原值,导致内容无法填满新暴露的区域。反之,当地址栏再次出现时,视觉视口缩小,但 100vh 仍然不变,就可能把底部元素挤出屏幕。
本质上,传统的视口单位 vw、vh、vmin、vmax 都基于静态的布局视口,完全忽略了动态 UI 带来的变化。
3. 现代CSS解决方案:动态视口单位 dvh、svh、lvh
现代浏览器(Chrome 108+、Safari 15.4+、Firefox 101+)引入了全新的一套视口单位,专门解决动态 UI 问题:
svh/svw(Small Viewport):代表最小可能视口,即地址栏和工具栏完全展开时的视口尺寸。使用100svh可以保证页面内容在任何时候都不会被工具栏遮挡,但工具栏隐藏时底部会留白。lvh/lvw(Large Viewport):代表最大可能视口,即地址栏和工具栏完全折叠时的视口尺寸。使用100lvh可以占满整个屏幕,但工具栏出现时底部内容会被遮挡。dvh/dvw(Dynamic Viewport):会实时跟随工具栏的动态变化而改变。当工具栏隐藏时,100dvh自动变大;工具栏出现时,自动缩小。这正是我们梦寐以求的行为。
最佳实践:直接使用 dvh 替代 vh。
.full-page {
height: 100vh; /* 降级方案,支援不兼容的浏览器 */
height: 100dvh; /* 现代浏览器的动态高度 */
}
对于大多数场景,100dvh 就能完美解决截断问题,并且无需 JavaScript。
4. 更稳定的兼容方案:使用 -webkit-fill-available 和 CSS 自定义属性
如果你需要更广泛的兼容性,或者对 dvh 的行为仍不满意(某些 Android 设备上 dvh 在软键盘弹出时仍可能异常),可以利用 -webkit-fill-available 特性。
-webkit-fill-available 是 WebKit 引擎的一个私有属性,它能够让元素占据剩余的所有可用空间,并且会动态响应地址栏的显示/隐藏。这个属性在 iOS Safari 和大多数移动端 Chrome 中都有效。
搭配自定义属性的稳健写法:
html {
/* 使整个文档的可用高度动态化 */
height: -webkit-fill-available;
}
body {
/* 保证背景色铺满,防止底部留白 */
min-height: 100vh;
min-height: -webkit-fill-available;
}
.full-height-container {
height: 100vh;
height: -webkit-fill-available;
}
这种方案无需脚本,且对旧版 iOS Safari 支持极好,是很多大型项目采用的降级策略。
5. JavaScript 动态计算方案:确保万无一失
如果你必须精确控制高度,或需要兼容一些极老的设备,JavaScript 是最终的可靠手段。通过监听 resize 事件,手动获取视觉视口的高度,并设置一个 CSS 变量。
<style>
.full-page {
/* 使用 CSS 变量,未定义时回退到 100vh */
height: calc(var(--vh, 1vh) * 100);
}
</style>
<script>
function setViewportHeight() {
// visualViewport 提供精确的可见视口高度(包括地址栏变化)
const vh = window.visualViewport
? window.visualViewport.height * 0.01
: document.documentElement.clientHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
// 初始化
setViewportHeight();
// 监听窗口大小变化以及 visualViewport 变化(工具栏显隐、键盘弹出)
window.addEventListener('resize', setViewportHeight);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setViewportHeight);
window.visualViewport.addEventListener('scroll', setViewportHeight);
}
</script>
这个方案的优势是完全基于真实可见区域,无论工具栏、键盘如何变化,--vh 始终是当前视觉视口高度的 1/100。你可以放心地用在任何全屏容器上。
6. 实战:构建一个全屏占满的页面
下面提供一个综合了多种方案的完整示例。它会优先使用 100dvh,不兼容时回退到 -webkit-fill-available,最后再用 JavaScript 动态变量兜底。你可以直接复制到项目中测试。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>100vh 移动端完美填充示例</title>
<style>
/* 重置与基础 */
* { margin: 0; padding: 0; box-sizing: border-box; }
html {
height: -webkit-fill-available;
background: #0f172a;
}
body {
min-height: 100vh;
min-height: -webkit-fill-available;
font-family: system-ui, sans-serif;
color: white;
}
/* 主容器:占满整个动态视口 */
.hero {
height: 100vh; /* 古早浏览器回退 */
height: 100dvh; /* 现代动态单位 */
height: -webkit-fill-available; /* WebKit 回退 */
height: calc(var(--vh, 1vh) * 100); /* JS 终极方案 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
p { font-size: 1.2rem; opacity: 0.9; max-width: 500px; }
/* 底部固定栏,确保总是在可见区域底部 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(10px);
display: flex;
justify-content: space-around;
align-items: center;
}
</style>
</head>
<body>
<div class="hero">
<h1>你好,世界</h1>
<p>这个页面会在任何手机上完美填满屏幕,无论地址栏是显示还是隐藏。</p>
</div>
<div class="bottom-nav">
<span>首页</span>
<span>搜索</span>
<span>我的</span>
</div>
<script>
(function() {
function setVH() {
let vh = window.innerHeight * 0.01;
// 如果支持 visualViewport,优先使用以获得更精确值
if (window.visualViewport) {
vh = window.visualViewport.height * 0.01;
}
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setVH();
window.addEventListener('resize', setVH);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setVH);
window.visualViewport.addEventListener('scroll', setVH);
}
})();
</script>
</body>
</html>
7. 总结与最佳实践
面对 100vh 移动端截断问题,推荐根据你的项目需求选择方案:
- 优先使用
dvh:如果你的目标用户大多使用较新的 iOS / Android 设备,直接从100vh改为100dvh是最简单、性能最好的选择。不要忘记带上100vh作为降级。 - 需要稳健兼容时:将
-webkit-fill-available与dvh组合,并配合min-height确保背景填充。 - 需要绝对精准或兼容老旧设备:采用 JavaScript 动态计算视觉视口高度,并使用 CSS 变量注入。这是最彻底的保障。
记住,永远不要假设移动视口是静态的。用户的每一次滑动、键盘的每一次弹起,都在改变真实的“窗口高度”。理解并善用这些新特性和兼容技巧,你的全屏页面就能在任何设备上表现得优雅且稳固。