发布时间:2025年11月3日
在业务上产品提出一个需求,对一个对比卡片组件中,当用户向下滚动页面时,表头会固定在可视区域顶部(通常通过 position: sticky 实现),同时给表头下面添加添加阴影效果,以提供视觉层次感,让用户清楚地知道内容已经滚动,表头下方还有更多内容。
position: sticky 可以让元素固定,但没有对应的 CSS 伪类来表示”已经进入 sticky 状态”:stuck 伪类不存在:stuck 伪类,但至今未被标准化:has() 可能可以部分实现类似效果/* ❌ 无法实现: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);
}
利用 Intersection Observer API 监听一个”哨兵元素”(sentinel element)的可见性变化,以此判断页面是否已经滚动。
在表格上方放置一个高度为 0 的空 div:
<div ref={headerRef} style={{ height: 0 }} />
关键点:
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 变为 falsesetIsHeaderOutOfView(true)threshold: 0 表示元素完全不可见时触发回调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>
此方案适用于以下场景:
observer.disconnect() 清理资源threshold 参数