现象
一个用 [data-scroll-reveal] 做“进入视口渐入”的列表页:正常浏览一切优雅。但三个场景暴露了问题——
- 用户 Ctrl+P 打印或某些 PDF 快照工具抓取时,未滚动到的下方区块整块空白;
- JS 被扩展或网络问题中断时,元素停在 CSS 初始的
opacity:0,读者看到一片“有字但看不见”的区域; - 个别元素因布局时机(图片撑高后已全在视口外上方)永远不触发 IO 回调,永久隐藏。
根因是同一个:进场动画默认把“未揭示”设成了视觉不可见,却没有为“揭示逻辑没跑成”留后路。
修复三件套
一、打印兜底——打印媒体里强制全部可见:
@media print {
[data-scroll-reveal] {
opacity: 1 !important;
transform: none !important;
}
}
二、自愈定时器——任何环境下,超时未揭示就直接标记为已揭示,绝不永久隐藏:
window.setTimeout(() => {
document
.querySelectorAll('[data-scroll-reveal]:not([data-revealed])')
.forEach((el) => markRevealed(el));
}, 6000);
三、reduced-motion / 无 IO 直接静态可见——初始化时判定,不注册观察者:
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
if (prefersReduced || !('IntersectionObserver' in window)) {
document.querySelectorAll('[data-scroll-reveal]').forEach(markRevealed);
return;
}
原则:动效是增强,不是内容显示的前提
一条通用铁律——内容的最终可见性不能依赖任何 JS 回调成功执行。CSS 的“初始隐藏态”必须同时满足:
- 打印/PDF:覆盖为可见;
- JS 关闭/报错:要么初始就可见(渐进增强),要么有
<noscript>或超时兜底; - 减弱动效偏好:直接可见;
- 观察器异常:自愈超时。
把进场动画想成“锦上添花的延迟出现”,而不是“内容的开关”。开关永远握在无 JS 也能成立的路径里。
验证
- 浏览器开发面板切换
prefers-reduced-motion,确认全部区块立即可见; - 模拟 JS 抛错(控制台临时 throw),确认内容仍显示;
- 打印预览确认无空白块。这三条都能用 Playwright 固化(本项目在 QA 轮做了 6 页 reduced-motion 免滚动检查,即基于此)。