可滚动页面背景填不满:`height: 100vh` vs `min-height: 100vh`

一次真实排查的复盘。场景为 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;
    }
}

两处改动:

  1. 核心height: 100vhmin-height: 100vh,让背景随内容铺满。
  2. 兜底 :背景色挂到 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 自然就清楚了。

相关推荐
Patrick_Wilson1 小时前
Squash Merge 的血缘陷阱:为什么删掉的代码又活了过来
前端·git·程序员
kyriewen2 小时前
今天的科技圈,全在抢英伟达的饭碗
前端·面试·ai编程
SouthernWind2 小时前
RAGFlow——结合本地知识库检索开发实战指南(包含聊天、检索本地的知识库文档和Agent模式)
前端
三翼鸟数字化技术团队2 小时前
websocket及SSE原理解析
前端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(八):设计管理员模型、热重载配置
前端·后端·go
政采云技术3 小时前
Chrome 高阶调试技巧
前端
牧艺3 小时前
HTML-in-Canvas 深度解析:让 Canvas 真正「吃上」HTML 这碗饭
前端·html·canvas
秦瑜华3 小时前
前端页面添加AI自动翻译按钮
前端·openai·ai编程