为什么同一个 div,在手机上能往上拖动?—— 移动端视口高度踩坑记录

本文最后更新于 2026年8月5日 凌晨

翻车现场

一个扫码页面,在电脑浏览器上看着一切正常——卡片撑满屏幕,底部对齐手机 home 指示线。

切到手机上,卡片高度比屏幕多出了几十像素。没有滚动条,但手指能从下往上拖动,页面会回弹。看起来像是:内容比视口”大了一圈”,但又没有大到需要滚动条。

同一个 div,同一套 CSS,PC 完美、手机溢出。

第一轮排查:是不是父容器没撑高

直觉反应:肯定有某个父容器没设 h-full,导致子元素的高度计算链断了。

我翻了一遍高度链:<body> → flex 布局 → main 区域 → 页面组件 → 卡片 div。

<body> 写了 h-screen,main 区域是 flex-1,卡片也是 h-full。结构看着没毛病,但手机就是溢出。

问题不在撑不够,而在撑过头了。

第二轮排查:加 max-h 限制

我给卡片加上了 max-h-[calc(100vh - 8rem)],意思是”卡片最高不要超过视口减 8rem”。

看起来修好了。但打开第二个扫码页面一比较——两个页面卡片高度不一致。一个是真正的撑满,一个被 max-h 截断了。

问题不是单个页面的高度,而是每个页面对”视口”的理解不一样。

第三轮:发现根因——100vh 在手机上不等于屏幕

我把 h-screen(即 height: 100vh)从 <main> 上移除后,溢出大幅缩小了,但还是有。

这才是真正的根因:100vh 在移动端浏览器中,永远是手机物理屏幕的总高度,包括浏览器地址栏、底部导航栏。

当手机浏览器的地址栏折叠后,实际可见的视口高度(viewport)比 100vh 小了大约 56~80px。所以 body { height: 100vh } 设的高度,比实际能显示的区域还要大。

这就是为什么”没有滚动条但能往上拖动”。CSS 计算的内容高度确实比视口大,但差得不是一屏。浏览器认为不需要滚动条,手指拖拽时却能感觉到超出。

最终方案:dvh(Dynamic Viewport Height)

CSS 有一个专门针对移动端设计的单位:dvh(Dynamic Viewport Height)。

  • 100vh = 物理视口(包含地址栏等浏览器 UI)
  • 100svh = 最小视口(地址栏展开时的最小可用高度)
  • 100dvh = 动态视口(随地址栏折叠/展开动态变化)

解决方案只有一行代码:

1
2
3
4
5
// 原来
<body className="flex flex-col h-screen">

// 改为
<body className="flex flex-col h-dvh">

同时确保整条高度链完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body className="flex flex-col h-dvh">
<AppHeader />
<div className="flex flex-1 overflow-hidden">
<AppSidebar />
<main className="flex-1 min-w-0 overflow-y-auto">
{/* 卡片 */}
<div className="h-full flex flex-col bg-background rounded-lg overflow-hidden">
<div className="shrink-0">头部</div>
<div className="py-5">输入区</div>
<div className="flex-1 overflow-auto">内容</div>
</div>
</main>
</div>
</body>

关键点:

  • bodyh-dvh 而不是 h-screen
  • mainflex-1 而不是 h-screen(flex 布局下混用 flex-1 + h-screen 会导致高度冲突)
  • 卡片高度链:h-full(卡片外层)→ flex-1(内容区)
  • 不要在中途用 h-full w-full 的包裹层打断 flex 高度传递

踩坑记录

坑一:flex 布局下 h-screenflex-1 冲突

现象:加了 h-screen 后页面反而更严重地超出视口。

根因flex-1 在 flex 容器内会撑满剩余空间,h-screen 是绝对值 100vh。两个属性同时存在时,h-screen 会覆盖 flex-1 的计算结果,导致子元素高度超过父容器。

解法:在 flex 布局中,父容器高度用 h-dvhh-full,子元素用 flex-1,不要同时用。

坑二:高度链在中途断掉

现象<body> 到卡片之间的某个 div 没有正确传递高度。

根因<main> 外层有一个 <div className="flex flex-1 overflow-hidden">,这个 div 需要父容器有明确高度才能撑开。<body> 改了 h-dvh 后高度就确定了。

解法:每层父容器都有明确的高度(h-dvhflex-1h-full),不能有遗漏。

坑三:h-full w-full 包裹层

现象:页面结构中间有一层 <div className="h-full w-full">,看似无害,但在某些 flex 布局下会中断高度传递。

根因h-full 需要父容器有明确高度才能生效。如果父容器是 flex-1,在某些浏览器/布局场景下 h-full 会退化为 auto

解法:在 flex 子元素里直接用 flex-1 撑高,而不是用 h-full

总结

移动端页面的”高度踩坑”,归根结底是两个问题:

  1. 视口单位选错了——100vh 不等于”手机能看到的屏幕高度”,要用 100dvh
  2. 高度链不连续——<body> 到目标元素之间,任何一层没撑满,整个链就断了

一行 h-dvh,加一条完整的 flex 高度链,问题就彻底解决了。


参考文献

  1. MDN: viewport-percentage lengths - dvh, lvh, svh
  2. MDN: Overflow

为什么同一个 div,在手机上能往上拖动?—— 移动端视口高度踩坑记录
https://normdist.com/2026/08/05/ND-20260805-001-mobile-viewport-height-chain/
作者
小瑞
发布于
2026年8月5日
许可协议