📚 离职知识库

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 下划线重排)时才值得引入这个依赖。


#四、踩坑清单

  1. 绝对不要动 height。改高度会触发 layout + paint,滚动时肉眼可见抖动。用 padding 撑高度、用 transform: scale() 缩元素。
  2. scale 必须配 transform-origin。默认从中心缩,图标会往左上飘。仿 Apple 用 left center
  3. A、B 两条路线不能同时生效。都开会互相打架(transition 和 animation 抢同一个属性)。demo 里的做法:@supports (animation-timeline: scroll()) 内把 transition 置为 none,JS 侧用 CSS.supports() 检测,支持就完全不挂 observer。
  4. 折叠要有明确终点。别写成「滚多少缩多少」,到了 compact 高度就必须停住,否则顶栏会一直缩到没。animation-range 的终点值就是这个终点。
  5. 底边线要延后出现。展开态带一条线会显得脏,折叠到位再浮现才像 Apple。
  6. 临界点抖动。路线 B 如果阈值刚好卡在 header 高度上,会出现加 class → 高度变小 → 又满足条件 → 移除 class 的循环。解法是 rootMargin 留出余量,或用哨兵元素(demo 用的就是哨兵)。
  7. backdrop-filter 要带 -webkit- 前缀,且父级不能有 overflow: hidden,否则毛玻璃失效。
  8. 无障碍:必须写 @media (prefers-reduced-motion: reduce),直接给终态、去掉所有过程动画。
  9. 移动端:窄屏下 tabs 要能横向滚(overflow-x: auto),否则折叠后挤成一团。
  10. position: sticky 失效时,八成是某级祖先带了 overflow: hidden/auto。往上一层层查。

#五、复用步骤

  1. 打开 折叠导航栏-demo.html,滚动确认效果,右下角徽标会显示当前跑的是哪条路线。
  2. <style> 里从「顶部吸顶导航栏」到「无障碍」之间的段落整体拷进目标项目。
  3. :root 里的 4 个变量对齐项目设计:--pad-expanded--pad-collapsed--collapse-range、配色。
  4. 把 header 的 DOM 结构换成项目自己的 logo / 导航项,保留 .inline-title 这个节点。
  5. 页面主标题加上 .page-title 类,并确保它在 main 里、是 .header 的后续兄弟节点的后代(路线 B 的 ~ 选择器依赖这个结构)。
  6. 如果项目是 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)