SOP:折叠式吸顶导航栏(Collapsing Sticky Header)
复刻对象:App Store Connect / iCloud 顶栏,本质是 iOS「Large Title → Inline Title」在 Web 端的实现。 配套 demo:同目录
折叠导航栏-demo.html,双击浏览器打开即可看效果。 沉淀日期:2026-08-14
#一、这个效果叫什么
| 场景 | 叫法 |
|---|---|
| 通用英文名 | collapsing sticky header / shrinking header |
| 同义别名 | condensed header、sticky shrink nav、scroll-linked header collapse |
| Apple 体系内 | Large Title → Inline Title 折叠(iOS UINavigationBar.prefersLargeTitles) |
| 中文 | 折叠式吸顶导航栏 |
查资料时用英文名搜,中文关键词几乎搜不到有用的东西。
设计意图:页面顶部时用大标题明确「你在哪」,一旦开始滚数据就把垂直空间让出来,同时保留一条细窄的常驻导航。
#二、动效拆解(照着这张表实现就不会走样)
| 元素 | 展开态 | 收起态 | 用什么属性做 |
|---|---|---|---|
| 顶栏容器 | padding-block: 22px |
padding-block: 7px |
padding(不要动 height) |
| 底部分割线 | 透明 | 1px 实线 | box-shadow: 0 1px 0 |
| 背景 | 半透明 | 半透明 + 毛玻璃 | backdrop-filter: saturate(180%) blur(20px) |
| App 图标 | 42px | 视觉约 26px | transform: scale(.62) + transform-origin: left center |
| App 名 | 26px | 19px | font-size |
| 页面大标题 | 显示 | 淡出上移 | opacity + transform |
| 顶栏内联小标题 | 隐藏 | 淡入 | opacity + transform |
折叠行程建议 100–120px,太短会显得生硬,太长用户滚半天没到位。
#三、三条实现路线,按这个顺序选
#路线 A:CSS scroll-driven animations(首选)
零 JS,动画跑在合成器线程上,滚动多快都不掉帧。
.header{
position: sticky; top: 0;
animation: h-shrink linear both;
animation-timeline: scroll(root block); /* 绑定到根滚动容器 */
animation-range: 0 110px; /* 前 110px 内完成折叠 */
backdrop-filter: saturate(180%) blur(20px);
}
@keyframes h-shrink{
to{ padding-block: 7px; box-shadow: 0 1px 0 #d2d2d7; }
}- 支持情况:Chrome/Edge 115+、Safari 26+(较早的 Safari 需前缀或不支持)、Firefox 需开 flag。
- 不支持的浏览器会自动退化成一条普通 sticky 顶栏,页面不会坏 —— 这是它最大的优点。
- 多个元素共用同一条时间线时,把
animation-timeline / animation-range / fill-mode抽到一个选择器组里写一次即可。
#路线 B:IntersectionObserver 哨兵(兼容性兜底)
在 header 上方放一个 1px 的空 div 当哨兵,它滚出视口就给 header 加 class:
const io = new IntersectionObserver(
([e]) => header.classList.toggle('is-collapsed', !e.isIntersecting),
{ rootMargin: '-56px 0px 0px 0px' } // 顶到收起后的高度才算滚出去
);
io.observe(sentinel);剩下交给 CSS transition。比监听 scroll 事件性能好得多,也不用 rAF 节流、不用手写阈值判断。
#路线 C:GSAP ScrollTrigger(折叠还要联动别的东西时)
gsap.to('.header', {
paddingBlock: 7,
scrollTrigger: { start: 0, end: 110, scrub: true }
});scrub: true 把整段折叠绑到滚动进度上,能做出「跟手」的中间态。只有当折叠过程需要同时驱动多个元素做复杂位移(图标飞到某个位置、tab 下划线重排)时才值得引入这个依赖。
#四、踩坑清单
- 绝对不要动
height。改高度会触发 layout + paint,滚动时肉眼可见抖动。用padding撑高度、用transform: scale()缩元素。 scale必须配transform-origin。默认从中心缩,图标会往左上飘。仿 Apple 用left center。- A、B 两条路线不能同时生效。都开会互相打架(transition 和 animation 抢同一个属性)。demo 里的做法:
@supports (animation-timeline: scroll())内把transition置为none,JS 侧用CSS.supports()检测,支持就完全不挂 observer。 - 折叠要有明确终点。别写成「滚多少缩多少」,到了 compact 高度就必须停住,否则顶栏会一直缩到没。
animation-range的终点值就是这个终点。 - 底边线要延后出现。展开态带一条线会显得脏,折叠到位再浮现才像 Apple。
- 临界点抖动。路线 B 如果阈值刚好卡在 header 高度上,会出现加 class → 高度变小 → 又满足条件 → 移除 class 的循环。解法是
rootMargin留出余量,或用哨兵元素(demo 用的就是哨兵)。 backdrop-filter要带-webkit-前缀,且父级不能有overflow: hidden,否则毛玻璃失效。- 无障碍:必须写
@media (prefers-reduced-motion: reduce),直接给终态、去掉所有过程动画。 - 移动端:窄屏下 tabs 要能横向滚(
overflow-x: auto),否则折叠后挤成一团。 position: sticky失效时,八成是某级祖先带了overflow: hidden/auto。往上一层层查。
#五、复用步骤
- 打开
折叠导航栏-demo.html,滚动确认效果,右下角徽标会显示当前跑的是哪条路线。 - 把
<style>里从「顶部吸顶导航栏」到「无障碍」之间的段落整体拷进目标项目。 - 改
:root里的 4 个变量对齐项目设计:--pad-expanded、--pad-collapsed、--collapse-range、配色。 - 把 header 的 DOM 结构换成项目自己的 logo / 导航项,保留
.inline-title这个节点。 - 页面主标题加上
.page-title类,并确保它在main里、是.header的后续兄弟节点的后代(路线 B 的~选择器依赖这个结构)。 - 如果项目是 React:把折叠逻辑收进一个
useCollapsingHeader()hook,内部只做「支持检测 + 必要时挂 observer」,class 名通过返回值给出。CSS 部分原样不动。
#六、React / Next.js 版最小骨架
'use client';
import { useEffect, useRef, useState } from 'react';
export function useCollapsingHeader() {
const sentinel = useRef<HTMLDivElement>(null);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
// 支持 CSS 滚动时间线的浏览器完全交给 CSS,JS 不介入
if (CSS.supports('animation-timeline', 'scroll()')) return;
if (!sentinel.current) return;
const io = new IntersectionObserver(
([e]) => setCollapsed(!e.isIntersecting),
{ rootMargin: '-56px 0px 0px 0px' }
);
io.observe(sentinel.current);
return () => io.disconnect();
}, []);
return { sentinel, collapsed };
}用法:<header className={collapsed ? 'header is-collapsed' : 'header'}>,紧跟一个 <div ref={sentinel} className="sentinel" />。
来源:沉淀/折叠导航栏动效/折叠导航栏-SOP.md(整理于 2026-08-18)