iOS 26+ Liquid Glass 顶部布局 · 色彩示意

左:实际比例(手机原型)。中:top 100px 放大 5 倍的"剖面图"。右:图例 + 图层开关。

实际比例 · iPhone 模拟
9:41 ●●●●● 100%
--safe-area-top · 44px
业务 safe area 起点 · 54px
wrapper · fixed; top:0 y=0 ~ 104
顶部剖面(系统层 vs DOM 层分开看)
A · 系统覆盖层(material compositing · OS / Safari 画在 webpage 之上)
iOS 状态栏 glyph绘制在 material 之上(不是独立 opaque 层) y=0~44 · material 浓
──── --safe-area-top ──── y ≈ 44px(glyph 区下沿,非 material 边界)
Material falloff同一层 material 继续衰减,最易"漏色" y=44~54 · 半透明
⬇ A 在 y=0~54 完全叠盖在 B 之上 ⬇
webpage 任何 z-index 都盖不掉 A
B · webpage DOM(wrapper 真实子节点)
wrapper(fixed; top:0; DOM 真实 104px)
10px LG spacer y=0~10 · 透明
44px safe-area filler y=10~54 · 透明
──── 业务 safe area 起点 ──── y ≈ 54px
页面内容 y=104+
为什么需要 MetaColor 蒙版?

iOS 26+ Liquid Glass 把状态栏顶边 ~6px 也做了半透明处理——webpage 背景色会从那一小条漏出来, 视觉上经常和正在滚动的页面色(绿、白、卡片色…)混在一起,状态栏的时间/电池看起来像叠在一片杂色背景上。

标准方案:<meta name="theme-color">
这是 W3C 标准,Safari 会用它给自己半透明 chrome 染色。MetaColor 组件会先写这个 meta

补充方案:6px fixed 蒙版(z:10100)
theme-color 有三个限制,所以还需要一个 webpage 层的补充方案:

  • 不支持 transparent——页面想让顶部"透明"时 meta 只能回落到默认色,无法跟随真实背景
  • 渐变/动画切色无法实时跟随——meta 更新有节流,路由切换、抽屉滑动等场景里色块会出现可见跳变
  • 染色范围由 Safari 决定——Apple 可能选择性地只染部分 chrome,漏掉某些边角

所以 MetaColor 走双保险:meta 让 Safari 配合染(系统层),6px DOM 蒙版自己再画一层(webpage 层)。 在 meta 漏掉、不响应或不支持的场景下,6px 蒙版都能补足。

显示/隐藏图层(直观对比作用)

A · 系统覆盖层(material compositing)
B · webpage DOM

图例

A · 系统覆盖层(material compositing)
Liquid Glass material(一整层,0~54px) iOS 26+ 把整个 top chrome 做成统一 material:顶 ~6px 与 44~54 falloff 都半透明(webpage 漏色就发生在这两段),中间浓。不是 "opaque 状态栏 + 单独透出带" 两段拼接。
9:41
状态栏 glyph 时间 / 电池 / 信号——OS 画在 material 之上,不是独立的 opaque 层。glyph 区高度 ~44px = --safe-area-top,material 在它下面继续延伸 10px 到 falloff。
B · webpage DOM
MetaColor 蒙版 fixed; top:0; height:6px; z-index:10100
App 根上挂的兄弟 DOM。物理上落在状态栏内部,靠 z-index 把漏色统一染成蒙版色。
wrapper(紫色虚框) fixed; top:0——从物理顶部 y=0 开始, 一直延伸到业务内容底边。
safe-area filler(wrapper 自带) wrapper 在 spacer 之后自动插入的 <div class="safe-area-inset-top">, 高度 = var(--safe-area-top) ≈ 44px。透明,被状态栏 + 透出带遮。 底色通过 safe-area-class prop 控制(例如 bg-theme-container-2)。
Safe 区域标注线 红虚线 = --safe-area-top(y=44);蓝虚线 = 业务 safe area 起点(y=54)。 纯标注,不是 DOM 元素。
wrapper 自带 spacer wrapper 第一个子节点,固定 10px。透明,被状态栏遮。 仅在 iOS 26+ Liquid Glass 时由 wrapper 自动插入。
NavBar(slot 可见内容) 被前 54px 透明占位推到业务 safe area 起点(蓝虚线)。
页面内容 可滚动正文。Liquid Glass 区域如果不盖蒙版,这块色就会透出来。

业务场景:在业务区域顶部 fix 一个广告

三种放置方式的视觉对比 + 推荐实现

❌ top: 0
SALE 50% OFF
9:41 ●●● 100%

广告 top: 0 被 iOS 状态栏完全盖住, 只剩底部一小条从 Liquid Glass 透出带漏出来(图中半透明部分), 视觉上等于不存在

❌ top: var(--safe-area-top)
SALE 50% OFF
9:41 ●●● 100%

广告顶到 safe-area 底边,顶边压在 Liquid Glass 透出带下, 那一条 8px 被半透明层遮盖模糊(颜色发淡、有 backdrop-blur), 在 iOS 26+ 设备上形成可见的视觉瑕疵。

✅ FixedWrapper 包裹
SALE 50% OFF
9:41 ●●● 100%

被 spacer 自动向下推 10px,顶边落在 Liquid Glass 透出带之下, 色块清晰;同时 placeholder 撑出对应高度,下方页面内容不会被遮。

推荐实现:用 <FixedWrapper> 包裹

项目里所有顶部 fixed 元素(NavBar、Tab、广告条、公告条…)都应该塞进同一个 <FixedWrapper>,让它统一处理 spacer / placeholder / fixed-to-sticky 切换。

✅ FixedWrapper 自动处理 safe-area:组件内部按顺序插入 10px Liquid Glass spacer → safe-area-inset-top filler → slot 可见内容。 消费方 slot 里只写真正可见的元素;filler 底色通过 safe-area-class prop 控制(比如让它跟下方 NavBar 同色)。

1. 单独 fix 一个广告(没有 NavBar)

<!-- baseHeight 仅传"slot 可见内容自身高度",wrapper 自动处理 spacer + safe-area filler -->
<FixedWrapper :baseHeight="50" safe-area-class="bg-orange-500">
  <div class="h-[50px] bg-gradient-to-r from-orange-500 to-pink-500">
    <span>🎁 SALE 50% OFF</span>
  </div>
</FixedWrapper>

2. NavBar + 广告 一起 fix(业务里最常见)

<!-- 50 (NavBar) + 36 (广告) = 86,base-height 直接写 86,区间在 40~200 之间都行 -->
<FixedWrapper :baseHeight="86" safe-area-class="bg-theme-card">
  <NavBar />          <!-- h-[50px] -->
  <AdBanner />        <!-- h-[36px] -->
</FixedWrapper>

baseHeight 只算可见内容(不含 safe-area filler)——因为 placeholder 镜像 wrapper 的三段高度(spacer + safe-area + slot),baseHeight 只需要给 wrapper "可见内容" 那一段。值域 40~200px,对应的 h-[Xpx] 类已通过 tailwind.config.js 的 safelist 预编译;超出区间需扩 safelist。

3. 不参与路由 sticky 切换的顶部 fixed(场景三)

<!-- :sticky-transition=false 跳过 .fixed-dom / .fixed-dom-placeholder 两个 class -->
<FixedWrapper :baseHeight="44" :sticky-transition="false">
  <PromoBanner />
</FixedWrapper>

非 wrapper 形态的浮动元素(比如 Banner 图、悬浮订单卡)继续走 useMetaTopOffset() 拿 10/0 偏移量自己拼 top: calc(var(--safe-area-top) + ${metaHeight}px)

关键要点