返回博客

微信小程序弹窗三段式 Flex:一个 min-height:0 救了整个布局

做一个底部筛选弹窗:顶部标题栏固定、中间列表可滚动、底部「重置 / 确定」固定。听起来简单,写起来却踩了一圈坑——header 随内容滚走、最后一个列表项被 footer 遮住、弹窗高度在 1 项和 100 项之间来回跳变。最后发现这些症状的根因几乎全指向同一处布局失误。

现象

在真机上打开弹窗,有时确认按钮压在最后一条数据上方,向下滚也滚不到底;有时标题栏在列表滑动时一起跑走;偶尔弹窗整体高度随数据量变化——数据少时弹窗矮一截,数据多时才撑到 70vh。模拟器表现正常,真机必现。

根因

症状看上去各自独立,但背后是一套连锁的布局错误:

错误 1:max-height 替代 height 导致内层 height:100% 解析为 0

<!-- 错误写法 -->
<van-popup custom-style="max-height: 70vh;">
  <view style="height: 100%; display: flex; flex-direction: column;">
    ...
  </view>
</van-popup>

max-height 是一个上界约束,不是具体尺寸。内层 height: 100% 需要一个确定的父高度才能解析——iOS WebKit 在这里经常返回 0,整个 flex 容器坍塌。

错误 2:flex body 缺少 min-height: 0,内容溢出

Flex 规范中,flex 子元素的默认 min-heightauto(即内容的固有高度)。这意味着即使给 body 设了 flex: 1,当内容很多时,body 也不会缩小到 flex 剩余空间以内——它会撑破容器,导致整个弹窗内部滚动而不是 scroll-view 局部滚动。

错误 3:用 padding-bottom 给 footer 让位

/* 错误写法 */
.popup-body { padding-bottom: 120rpx; }

scroll-view 的可滚动区域会被 padding 挤压,最后一项实际上进入了 padding 的「死区」——用户滚到底也看不见它。footer 高度一旦变化(加了按钮、安全区高度不同),这个数字就需要手动维护,必然漂移。

坑:5 种写法在真机上都会出问题

以下写法全部禁用:

  1. van-popupmax-height 而非 height(内层 height:100% 解析为 0)
  2. .popup-bodymin-height: 0(flex 子元素默认 min-height: auto,撑破容器)
  3. .popup-body { padding-bottom: 120rpx } 给 footer 让位(最后一项进入滚动死区)
  4. footer 用 position: fixed/absolute(fixed 锚点是 viewport 而非弹窗,popup 动画时 footer 位置错乱)
  5. header 用 position: sticky(iOS WebKit 内嵌 WebView 中 sticky 对无明确高度的祖先行为不可靠)

正解

用一个 flex 三段式布局,popup 给定具体高度,三个区域各司其职:

<van-popup
  show="{{showPopup}}"
  position="bottom"
  round
  bind:close="onClose"
  custom-style="height: 70vh;">
  <view class="popup-wrap">
    <view class="popup-header">
      <text class="popup-title">按标签筛选</text>
      <van-icon name="cross" bindtap="onClose" />
    </view>
    <scroll-view class="popup-body" scroll-y>
      <!-- 列表内容,唯一的滚动区域 -->
    </scroll-view>
    <view class="popup-footer">
      <view bindtap="onReset">重置</view>
      <view bindtap="onConfirm">确定</view>
    </view>
  </view>
</van-popup>
.popup-wrap {
  display: flex;
  flex-direction: column;
  height: 100%;          /* 撑满 popup — 关键 */
  box-sizing: border-box;
}
.popup-header {
  flex-shrink: 0;        /* 不被压缩 — 关键 */
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 32rpx;
  border-bottom: 1rpx solid #f1f5f9;
}
.popup-body {
  flex: 1;               /* 占满剩余空间 — 关键 */
  min-height: 0;         /* 允许 flex 子元素缩小 — 关键 */
  padding: 0 32rpx;
}
.popup-footer {
  flex-shrink: 0;
  display: flex;
  gap: 24rpx;
  padding: 20rpx 32rpx;
  padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); /* 安全区在 footer */
  border-top: 1rpx solid #f1f5f9;
}

每一条都不能少:

| 属性 | 缺少后的症状 | |------|------------| | custom-style="height: 70vh" | 内层 height:100% 解析失败,flex 链断裂 | | .popup-wrap height: 100% | wrap 收缩到内容尺寸,body 无 flex 空间 | | .popup-header flex-shrink: 0 | 内容多时 header 被压缩到 0 | | .popup-body flex: 1 | body 不占剩余空间,无法滚动 | | .popup-body min-height: 0 | body 撑破容器,整体滚动而非局部滚 | | .popup-footer flex-shrink: 0 | footer 被压缩,安全区 padding 丢失 |

正解:popup 给 height 不给 max-height,body 加 flex:1 + min-height:0

核心记忆:max-height 是约束,不是具体高度,内层 height:100% 依赖具体高度才能解析。min-height:0 是让 flex 子元素能够缩小到 flex 分配空间以内的必要条件,缺它 body 永远撑破父容器。

安全区永远加在 footer,不要加在 body

env(safe-area-inset-bottom) 只放在 popup-footerpadding-bottom 里。如果加在 body 上,scroll-view 的滚动死区问题会复发。

一句话外卖

van-popup 弹窗布局记住两个关键词:外层用 height(不是 max-height),body 加 min-height: 0——其余五种反模式都由这两处疏忽衍生。