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:
- The first screen had the correct background color.
- After scrolling down, the content beyond the first screen (the last few cards) appeared on a white background. The background color did not fill the entire scrollable area.
┌──────────────┐
│ ▓▓▓ 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).
- Content shorter than one screen → looks normal.
- Content longer than one screen (requires scrolling) → the element is locked to one screen height, content overflows below; the background is only painted on this one screen, so the scrolled part naturally has no background.
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
.group-list { height: 100% }→ asks the parent.containerhow tall it is..containerhas no height set → it isauto(determined by content).- When the parent is
auto, the child'sheight: 100%becomes invalid / degrades toauto.
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":
- Short content → the minimum takes effect, filling one screen.
- Long content → the container grows with the content, and the background fills the entire scrollable area.
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:
- Core:
height: 100vh→min-height: 100vh, allowing the background to fill with the content. - Fallback: Apply the background color to the
pageroot node. In Mini Programs, the truly scrollable element ispage. 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
pageselector, even when written inscopedstyles, will not havedata-vscoping 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
height: 100vh— "Is exactly one screen". Suitable for non-scrollable first screens (full-screen banners, login page backgrounds).min-height: 100vh— "At least one screen, can grow". Suitable for lists / scrollable pages. ✅max-height: 100vh— "At most one screen, scrolls internally if exceeded". Suitable for modals, drawers.
3. Best Practice: First Determine "Who Scrolls"
- The entire page scrolls →
min-height: 100vh(this case); apply background topagefirst.- Header and footer are fixed, only the middle section scrolls internally → Outer container
height: 100vh(locks one screen) + middle sectionflex: 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.