为什么同一个 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 | |
同时确保整条高度链完整:
1 | |
关键点:
body用h-dvh而不是h-screenmain用flex-1而不是h-screen(flex 布局下混用flex-1+h-screen会导致高度冲突)- 卡片高度链:
h-full(卡片外层)→flex-1(内容区) - 不要在中途用
h-full w-full的包裹层打断 flex 高度传递
踩坑记录
坑一:flex 布局下 h-screen 和 flex-1 冲突
现象:加了 h-screen 后页面反而更严重地超出视口。
根因:flex-1 在 flex 容器内会撑满剩余空间,h-screen 是绝对值 100vh。两个属性同时存在时,h-screen 会覆盖 flex-1 的计算结果,导致子元素高度超过父容器。
解法:在 flex 布局中,父容器高度用 h-dvh 或 h-full,子元素用 flex-1,不要同时用。
坑二:高度链在中途断掉
现象:<body> 到卡片之间的某个 div 没有正确传递高度。
根因:<main> 外层有一个 <div className="flex flex-1 overflow-hidden">,这个 div 需要父容器有明确高度才能撑开。<body> 改了 h-dvh 后高度就确定了。
解法:每层父容器都有明确的高度(h-dvh、flex-1、h-full),不能有遗漏。
坑三:h-full w-full 包裹层
现象:页面结构中间有一层 <div className="h-full w-full">,看似无害,但在某些 flex 布局下会中断高度传递。
根因:h-full 需要父容器有明确高度才能生效。如果父容器是 flex-1,在某些浏览器/布局场景下 h-full 会退化为 auto。
解法:在 flex 子元素里直接用 flex-1 撑高,而不是用 h-full。
总结
移动端页面的”高度踩坑”,归根结底是两个问题:
- 视口单位选错了——
100vh不等于”手机能看到的屏幕高度”,要用100dvh - 高度链不连续——
<body>到目标元素之间,任何一层没撑满,整个链就断了
一行 h-dvh,加一条完整的 flex 高度链,问题就彻底解决了。
参考文献