🌑

Mocha's Blog

目录
  1. 问题场景
  2. 为什么纯 CSS 无法实现
    1. CSS 的局限性
    2. 尝试过的 CSS 方案
  3. 解决方案:空 div + IntersectionObserver
    1. 核心思路
    2. 实现步骤
      1. 1. 创建哨兵元素
      2. 2. 使用 IntersectionObserver 监听
      3. 3. 根据状态动态添加阴影
    3. 方案优势
    4. 适用场景
    5. 注意事项

小技巧之滚动后添加阴影效果

发布时间:2025年11月3日

问题场景

在业务上产品提出一个需求,对一个对比卡片组件中,当用户向下滚动页面时,表头会固定在可视区域顶部(通常通过 position: sticky 实现),同时给表头下面添加添加阴影效果,以提供视觉层次感,让用户清楚地知道内容已经滚动,表头下方还有更多内容。

为什么纯 CSS 无法实现

CSS 的局限性

  1. 缺少滚动状态检测
  • CSS 无法直接检测”页面是否已经滚动过某个位置”
  • position: sticky 可以让元素固定,但没有对应的 CSS 伪类来表示”已经进入 sticky 状态”
  1. :stuck 伪类不存在
  • CSS 规范中曾讨论过 :stuck 伪类,但至今未被标准化
  • 无法通过 CSS 选择器判断 sticky 元素是否已经”粘住”
  1. 浏览器兼容性问题
  • 一些现代 CSS 特性如 :has() 可能可以部分实现类似效果
  • 但在移动端和旧版浏览器中兼容性不佳

尝试过的 CSS 方案

/* ❌ 无法实现:CSS 没有滚动状态伪类 */
.header:scrolled {
  box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
}

/* ❌ 无法实现::stuck 伪类不存在 */
.header:stuck {
  box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
}

解决方案:空 div + IntersectionObserver

核心思路

利用 Intersection Observer API 监听一个”哨兵元素”(sentinel element)的可见性变化,以此判断页面是否已经滚动。

实现步骤

1. 创建哨兵元素

在表格上方放置一个高度为 0 的空 div:

<div ref={headerRef} style={{ height: 0 }} />

关键点

  • 高度设为 0,不占据实际空间,不影响布局
  • 位置在表头正上方,作为滚动检测的”触发点”

2. 使用 IntersectionObserver 监听

useEffect(() => {
  if (!headerRef.current) return;

  // 创建 IntersectionObserver 来监听 headerRef 的可见性
  const observer = new IntersectionObserver(
    ([entry]) => {
      // 当 headerRef 不可见时(滚动到上方),设置状态为 true
      setIsHeaderOutOfView(!entry.isIntersecting);
    },
    {
      // 当元素完全不可见时触发
      threshold: 0,
    },
  );

  observer.observe(headerRef.current);

  // 清理函数
  return () => {
    observer.disconnect();
  };
}, []);

工作原理

  • IntersectionObserver 监听哨兵元素是否在视口内
  • 当用户向下滚动,哨兵元素滚出视口时,entry.isIntersecting 变为 false
  • 此时更新状态 setIsHeaderOutOfView(true)
  • threshold: 0 表示元素完全不可见时触发回调

3. 根据状态动态添加阴影

const headerShadowStyle = isHeaderOutOfView
  ? {
    boxShadow:
      '0px 0px 0.03rem -0.01rem rgba(18, 49, 113, 0.04), 0px 0.11rem 0.24rem -0.09rem rgba(18, 49, 113, 0.14)',
  }
  : {};

将计算出的样式应用到表头的每个 <th> 单元格上:

<tr className={styles.headerRow}>
  <th className={styles.dimensionColumn} style={headerShadowStyle}>
    产品名称
  </th>
  <th className={styles.firstProductColumn} style={headerShadowStyle}>
    {products[0]?.productName}
  </th>
  <th className={styles.secondProductColumn} style={headerShadowStyle}>
    {products[1]?.productName}
  </th>
</tr>

方案优势

  1. 性能优秀:IntersectionObserver 是浏览器原生 API,性能优于 scroll 事件监听
  2. 逻辑清晰:通过哨兵元素将”滚动检测”转化为”可见性检测”
  3. 兼容性好:IntersectionObserver 在现代浏览器中支持良好
  4. 无副作用:哨兵元素高度为 0,不影响页面布局

适用场景

此方案适用于以下场景:

  • sticky 表头需要在滚动后添加阴影
  • 固定导航栏需要滚动后改变样式
  • 任何需要检测”是否已滚动过某个位置”的场景

注意事项

  1. 哨兵元素必须放在需要检测的位置上方
  2. 确保在组件卸载时调用 observer.disconnect() 清理资源
  3. 如果需要更精确的触发时机,可以调整 threshold 参数

Powered By Hexo.js Hexo and Minima. Support By Oracle & Docker-Compose.