首页封面不再是一张静态海报:它黏在视口里,内容从下方覆盖上来,封面文字随滚动淡出、上移。实现只用了一个 sticky、几个 CSS 变量和一段 rAF。
这次首页改版,最先确定下来的不是颜色,而是一个动作:向下滚动时,封面应该像舞台幕布一样退场,而不是整块被推走。
最后的实现并不复杂,但有几个容易被忽略的层级问题。
先决定谁覆盖谁
页面结构保持简单:
<main>
<section class="hero">…</section>
<div class="home-body">
<div class="marquee">…</div>
<section class="latest">…</section>
<section class="channels">…</section>
<section class="about">…</section>
</div>
</main>
封面本身先正常占据一屏。滚动开始后,它通过 position: sticky 留在视口顶部;后面 .home-body 有背景和更高的层级,于是从下方盖上来。
body.page-home .hero {
position: sticky;
top: 0;
z-index: 0;
height: 100svh;
min-height: 680px;
}
.home-body {
position: relative;
z-index: 2;
background: var(--bg);
box-shadow: 0 -26px 80px rgba(0, 0, 0, 0.42);
}
这里有三个关键点:
home-body必须有背景。 否则封面会从内容缝隙里透出来。z-index要分开。 封面在下,内容在上;固定头部再在更上层。- 祖先不能是滚动容器。 最初
body用了overflow-x: hidden,sticky 会受影响。改为overflow-x: clip后,横向仍然被裁切,但不会创建新的滚动上下文。
100svh 而不是 100vh,是为了在移动端地址栏收起/展开时,封面高度跟可视区域一致。
滚动进度:只做三件事
滚动时不改变布局,只改变三个视觉值:
const p = Math.min(1, Math.max(0, window.scrollY / (hero.offsetHeight * 0.88)));
const eased = p * p * (3 - 2 * p);
hero.style.setProperty("--cover-opacity", String(Math.max(0, 1 - eased * 1.32)));
hero.style.setProperty("--cover-shift", `${-72 * eased}px`);
hero.style.setProperty("--cover-media-opacity", String(1 - eased * 0.45));
hero.style.setProperty("--cover-scale", String(1.015 + eased * 0.045));
CSS 只负责消费变量:
.hero__inner {
opacity: var(--cover-opacity, 1);
transform: translate3d(0, var(--cover-shift, 0px), 0);
}
.hero__media {
opacity: var(--cover-media-opacity, 1);
}
.hero__media img {
transform: scale(var(--cover-scale, 1.015));
}
这样做的原因是:opacity 和 transform 不会触发重排,滚动时主要在合成层完成。文字和图片的淡出速度不同,形成一点层次,而不是整块一起消失。
实际在 1440×1000 的视口里,滚动到 420px 时测得:
- 封面透明度约 0.385
- 文字上移约 -33.55px
- 图片仍然保留约 0.79 的可见度
再往下,内容完全盖住封面。
rAF 与减少运动
滚动事件本身很密集,所以只做一次 requestAnimationFrame 节流:
let ticking = false;
window.addEventListener("scroll", () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
updateCover();
ticking = false;
});
}, { passive: true });
当 prefers-reduced-motion: reduce 时,不绑定这段滚动逻辑。封面仍然 sticky,内容仍然覆盖,但不再做位移和淡出。动画是增强,不是阅读的前提。
内容为什么只剩三块
改版前首页还有订阅横条、公告和多组入口。信息一多,覆盖滚动的节奏会被打散。
最后只保留:
- 最新文章:一主四辅,保持编辑感。
- 分类频道:三个入口,颜色和站点主色对应。
- 关于我:三张视觉素材 + 简介 + GitHub / 博客园。
不是这些内容不重要,而是首页应该先回答“这里有什么”,而不是一次把所有东西都摊开。
代价
覆盖式封面不是没有成本:
- 封面必须有一屏的固定高度,矮屏需要单独压缩字号和间距。
- sticky 对祖先的 overflow 敏感,调试时要逐层检查。
- 内容层必须有不透明背景,透明设计会和封面打架。
- 如果用户用键盘快速跳转,封面仍会在背后,需要保证焦点顺序不受影响。
但换来的效果是明确的:首页第一次有了“翻页”的感觉,而不是一串纵向堆叠的区块。
好的滚动效果不是让页面动得更多,而是让内容出现得更自然。