跪拜 Guibai
← Back to the summary

The 100vh Trap: Why Scrollable Pages Need `min-height`, Not `height`

A Real Debugging Postmortem: When a Scrollable Page's Background Doesn't Fill — height: 100vh vs min-height: 100vh

A real debugging postmortem. The scenario is uni-app (WeChat Mini Program), but the conclusion applies equally to regular web development.

1. The Problem (The Bug)

A group chat list page. The list container .group-list was given a background color and height: 100vh, with the expectation that no matter how you scroll, the entire page would have a uniform light gray background.

The actual result:

┌──────────────┐
│ ▓▓▓ Card 1 ▓▓▓ │  ← First screen, has background
│ ▓▓▓ Card 2 ▓▓▓ │
│ ▓▓▓ Card 3 ▓▓▓ │
├──────────────┤  ← 100vh boundary (background ends here)
│     Card 4     │  ← Revealed after scrolling, white background!
│     Card 5     │
└──────────────┘

Original styles (simplified):

.container {
    .group-list {
        width: 100%;
        height: 100vh;          // ← The problem
        background: #F7F8F9;
        padding: 4rpx 28rpx;
    }
}

2. Analysis Process

1. First, understand what vh is?

100vh = 100% of the viewport height, i.e., the height of "the currently visible screen".

Key point: It is a fixed value, completely unrelated to the actual length of the page content.

Example: The visible area is about 753px high, so 100vh ≈ 753px, it is always this number — even if the list has 50 cards and the real content is 3000px high, 100vh is still 753px.

2. Distinguish between two "heights"

Concept Meaning What locks it
Viewport height The height of the visible screen 100vh locks this
Content height The actual height of all content combined, which can far exceed one screen Determined by the amount of content

The essence of the problem emerges: Using a fixed height (viewport height) to contain content that keeps getting longer (content height).

So this is not "100vh has a bug", but rather "a fixed height is inherently unsuitable for variable-length scrollable content".

3. Verify another common misconception: Would changing to height: 100% work?

No, and it could be worse. Core rule: % height is calculated relative to the "parent element's height", not relative to the screen.

Structure chain: page → .container → .group-list

Result: Height is determined by content. If the content is short, it won't fill one screen, still showing a white background — losing the ability to "at least fill one screen".

What if we set the entire parent chain to 100%?

page       { height: 100%; }
.container { height: 100%; }
.group-list{ height: 100%; }

In this case, 100% is passed down layer by layer, ultimately equaling the screen height, which becomes a fixed one-screen height again, exactly the same as height: 100vh, and will still clip after scrolling. We've come full circle back to the starting point.

4. Find the correct solution: min-height

The semantics of min-height: 100vh is "at least one screen high, and if the content is longer, it grows with it":

3. The Result (Final Fix)

page {
    background: #F7F8F9;     // Fallback: the truly scrollable element is the page root
}

.container {
    .group-list {
        width: 100%;
        min-height: 100vh;   // height → min-height, the core fix
        background: #F7F8F9;
        padding: 4rpx 28rpx;
    }
}

Two changes:

  1. Core: height: 100vhmin-height: 100vh, allowing the background to fill with the content.
  2. Fallback: Apply the background color to the page root node. In Mini Programs, the truly scrollable element is page. Giving it a background color ensures a consistent full-screen background even in empty states, iOS rubber-band bounce, edge gaps, etc.

Note: In uni-app, the page selector, even when written in scoped styles, will not have data-v scoping added; it is specially handled and can be used with confidence.

4. Knowledge Summary

1. Comparison of Four Approaches

Approach Fills one screen with short content Fills background with long (scrollable) content
height: 100vh ❌ Locked to one screen, clips
height: 100% (parent has no height) ❌ Won't fill ✅ (Relies on content to stretch)
height: 100% (entire parent chain is 100%) ❌ Equivalent to 100vh, still clips
min-height: 100vh

In one sentence: The root of the problem is not vh vs %, but height (fixes a single value) vs min-height (a minimum that can grow).

2. How to Choose Height

3. Best Practice: First Determine "Who Scrolls"

  • The entire page scrollsmin-height: 100vh (this case); apply background to page first.
  • Header and footer are fixed, only the middle section scrolls internally → Outer container height: 100vh (locks one screen) + middle section flex: 1 / <scroll-view> + overflow-y: auto.
Layout with fixed header/footer and local scrolling (in this case, `height` is actually needed to lock):
┌──────────────┐
│  Fixed Top Nav  │  ← Doesn't scroll
├──────────────┤
│  Middle List Scrolls  │  ← Only this scrolls (flex:1 / scroll-view)
├──────────────┤
│  Fixed Bottom Button  │  ← Doesn't scroll
└──────────────┘

If min-height: 100vh is mistakenly used in this local scrolling scenario, the entire page will be stretched by the content, causing the header and footer to move out of place, which is counterproductive.

4. Extension: The Pitfall of vh on Mobile Browsers (H5 Pages)

In regular mobile browsers, 100vh sometimes includes the address bar height, making it taller than the actual visible area, causing the bottom to be obscured. Modern CSS uses 100dvh (dynamic viewport height) to solve this.

However, WeChat Mini Programs do not have a browser address bar, so 100vh is a stable one-screen height, and dvh is not needed. This case is purely a "fixed height vs. variable content" problem.

5. One-Sentence Summary

For pages that scroll, you should almost always use min-height instead of height. When CSS seems "not working", first go back and look at the structure — is it full-page scrolling or local scrolling? Once the structure is correctly identified, whether to use height or min-height becomes clear.