一次真实排查的复盘。场景为 uni-app(微信小程序),但结论对普通 Web 同样适用。
一、缘由(问题现象)
一个群聊列表页,给列表容器 .group-list 设了背景色和 height: 100vh,期望不管怎么滑动,整页都是统一的浅灰底色。
实际效果:
- 第一屏背景色正常;
- 往下滚动后,超出第一屏的内容(最后几张卡片)落在了白底上,背景色没有铺满整个可滚动区域。
text
┌──────────────┐
│ ▓▓▓ 卡片1 ▓▓▓ │ ← 第一屏,有底色
│ ▓▓▓ 卡片2 ▓▓▓ │
│ ▓▓▓ 卡片3 ▓▓▓ │
├──────────────┤ ← 100vh 边界(底色到此为止)
│ 卡片4 │ ← 滚动后露出,白底!
│ 卡片5 │
└──────────────┘
原始样式(简化):
scss
.container {
.group-list {
width: 100%;
height: 100vh; // ← 问题所在
background: #F7F8F9;
padding: 4rpx 28rpx;
}
}
二、分析过程
1. 先搞清楚 vh 是什么?
100vh = 视口(viewport)高度的 100%,也就是「当前可见的这一屏」的高度。
关键点:它是一个固定值,和页面实际内容多长完全无关。
举例:可视区域约 753px 高,那么 100vh ≈ 753px,永远是这个数 ------哪怕列表里有 50 张卡片、真实内容高 3000px,100vh 还是 753px。
2. 分清两个「高度」
| 概念 | 含义 | 谁锁定它 |
|---|---|---|
| 视口高度 | 屏幕可见的那一屏 | 100vh 锁的就是它 |
| 内容高度 | 所有内容加起来的真实高度,可能远超一屏 | 由内容多少决定 |
问题的本质浮现:用一个固定高度(视口高度),去装一个会不断变长的内容(内容高度)。
- 内容比一屏短 → 看起来正常;
- 内容比一屏长(需要滚动)→ 元素被锁死在一屏高度,内容溢出到下面;背景色只画在这一屏上,滚下去的部分自然没有底色。
所以这不是「100vh 出 bug 了」,而是「固定高度天生不适配可变长的可滚动内容」。
3. 验证另一个常见误解:换成 height: 100% 行不行?
不行,而且可能更糟。核心规则:% 高度是相对「父元素高度」算的,不是相对屏幕。
结构链路:page → .container → .group-list
.group-list { height: 100% }→ 去问父级.container多高;.container没设高度 → 它是auto(由内容撑开);- 父级是
auto时,子级height: 100%会失效 / 退化成auto。
结果:高度由内容决定。内容短就撑不满一屏,照样露白底------丢掉了「至少占满一屏」的能力。
那把父链都设成 100% 呢?
scss
page { height: 100%; }
.container { height: 100%; }
.group-list{ height: 100%; }
这时 100% 一层层传下来最终 = 屏幕高度,又变回固定的一屏高 ,和 height: 100vh 一模一样,滚动后照样裁切。绕一圈回到原点。
4. 找到正解:min-height
min-height: 100vh 的语义是「至少一屏高,内容更多就跟着长」:
- 内容短 → 下限生效,占满一屏;
- 内容长 → 容器随内容增高,背景铺满整个可滚动区域。
三、结果(最终修复)
scss
page {
background: #F7F8F9; // 兜底:真正可滚动的是 page 根节点
}
.container {
.group-list {
width: 100%;
min-height: 100vh; // height → min-height,核心修复
background: #F7F8F9;
padding: 4rpx 28rpx;
}
}
两处改动:
- 核心 :
height: 100vh→min-height: 100vh,让背景随内容铺满。 - 兜底 :背景色挂到
page根节点上。小程序里真正可滚动的是page,给它上底色后,空状态、iOS 橡皮筋回弹、边缘缝隙等情况也都能保证全屏底色一致。
注:uni-app 里
page选择器即使写在scoped样式中也不会被加data-v作用域,是被特殊处理的,可放心使用。
四、知识点沉淀
1. 四种写法对照
| 写法 | 短内容铺满一屏 | 长内容(滚动)背景铺满 |
|---|---|---|
height: 100vh |
✅ | ❌ 锁死一屏,裁切 |
height: 100%(父级无高度) |
❌ 撑不满 | ✅(靠内容撑) |
height: 100%(父链都 100%) |
✅ | ❌ 等价 100vh,仍裁切 |
min-height: 100vh |
✅ | ✅ |
一句话:问题的根不在 vh 还是 %,而在 height(钉死一个值)vs min-height(下限可生长)。
2. 高度该怎么选
height: 100vh------ 「就是一屏」。适合首屏不滚动的场景(全屏 banner、登录页背景)。min-height: 100vh------ 「至少一屏,能长」。适合列表 / 可滚动页面。✅max-height: 100vh------ 「最多一屏,超了内部自滚」。适合弹窗、抽屉。
3. 最佳实践:先看「谁来滚」
- 整个页面跟着滚 →
min-height: 100vh(本案例);背景优先挂page。- 头尾固定、只有中间一块内部滚 → 外层
height: 100vh(钉死一屏)+ 中间flex: 1/<scroll-view>+overflow-y: auto。
text
头尾固定、局部滚动的布局(此时反而要用 height 钉死):
┌──────────────┐
│ 固定顶部导航 │ ← 不滚
├──────────────┤
│ 中间列表滚动 │ ← 只有这里滚(flex:1 / scroll-view)
├──────────────┤
│ 固定底部按钮 │ ← 不滚
└──────────────┘
这种局部滚动若误用 min-height: 100vh,整页会被内容撑长,头尾跟着跑掉,反而坏事。
4. 延伸:vh 在移动端浏览器的坑(H5 页面)
普通移动端浏览器里,100vh 有时会把地址栏高度也算进去,导致比真正可见区域偏高、底部被遮挡。现代 CSS 用 100dvh(动态视口高度)解决。
但微信小程序没有浏览器地址栏 ,100vh 就是稳定的一屏,无需考虑 dvh。本案例纯粹是「固定高 vs 可变内容」的问题。
五、一句话总结
会滚动的页面,几乎都该用 min-height 而不是 height。 遇到 CSS「调不通」,先回头看结构 ------是整页滚动还是局部滚动,结构判断对了,该用 height 还是 min-height 自然就清楚了。