CSS 100vh 在手机上底部被截断

FreeGuideOnline 最新 2026-07-04

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 仍然不变,就可能把底部元素挤出屏幕。

本质上,传统的视口单位 vwvhvminvmax 都基于静态的布局视口,完全忽略了动态 UI 带来的变化。

3. 现代CSS解决方案:动态视口单位 dvhsvhlvh

现代浏览器(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 移动端截断问题,推荐根据你的项目需求选择方案:

  1. 优先使用 dvh:如果你的目标用户大多使用较新的 iOS / Android 设备,直接从 100vh 改为 100dvh 是最简单、性能最好的选择。不要忘记带上 100vh 作为降级。
  2. 需要稳健兼容时:将 -webkit-fill-availabledvh 组合,并配合 min-height 确保背景填充。
  3. 需要绝对精准或兼容老旧设备:采用 JavaScript 动态计算视觉视口高度,并使用 CSS 变量注入。这是最彻底的保障。

记住,永远不要假设移动视口是静态的。用户的每一次滑动、键盘的每一次弹起,都在改变真实的“窗口高度”。理解并善用这些新特性和兼容技巧,你的全屏页面就能在任何设备上表现得优雅且稳固。