滚动叠层转场
每一屏吸在顶部,下一屏爬上来盖住它。下面那层从不被推走 —— 它是被盖住、压暗、后退 6%,这才让「叠层」读起来像纵深而不是一个列表。
怎么做到的
- Every section is
position: sticky; top: 0; height: 100%. That is the entire mechanism — each one parks at the top and the next scrolls up over it in normal document order. No transforms are needed to move anything. - The section underneath does not move. Translating it up as it is covered is the instinct, and it is wrong: two things moving at once reads as a list scrolling past, not as one card landing on another.
- Depth comes from two cheap cues on the covered section — a dark veil to 0.62 and
scale(1 - k × 0.06)on its contents. 6% is small enough to feel like distance rather than like the card shrinking. kis measured from the next section's owngetBoundingClientRect().topdivided by the viewport height, giving 0 while it is off-screen and 1 when it has fully covered. Deriving it from the covering element rather than from a global scroll offset means it stays correct however the sections are sized.- Each section needs
isolation: isolate. Without it the sticky siblings bleed through each other's rounded corners — one of those artefacts that only shows on the exact overlap frame. - The last section carries no transition and simply rests. A stack that ends mid-transition leaves the page looking like it failed to finish loading.
参数
- Mechanism
- position: sticky; top: 0; height: 100%
- Corner
- 18px 18px 0 0
- Veil
- solid #05050a → 0.62
- Push back
- scale(1 − k × 0.06)
- Progress k
- 1 − nextRect.top / viewportHeight
提示词
把这段丢给任意一个写代码的模型,就能从零长出这个效果。它把每个数值都写死了 ——「丝滑」「现代感」这种词每次生成出来的都不一样。
Build a section stacking transition in vanilla HTML/CSS/JS. Give every full-height section position: sticky; top: 0; height: 100vh; border-radius: 18px 18px 0 0; overflow: hidden and isolation: isolate — the isolation is required or sticky siblings bleed through each other's rounded corners. The section being covered must NOT move: do not translate it. Instead compute, for each section, k = clamp(0, 1 - nextSection.getBoundingClientRect().top / viewportHeight, 1) and use it for two things only — a solid dark overlay at opacity k * 0.62, and scale(1 - k * 0.06) on that section's contents. Derive k from the covering element's own rect rather than from a global scroll offset so it stays correct at any section height. Give the last section no transition so the stack comes to rest. Register the scroll listener with { passive: true }.源码
整个文件。上面那个样板跑的就是它 —— 存成 .html 打开就能用,不需要别的任何东西。
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Section stacking transition</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif; }
.scroller { height: 100%; overflow-y: auto; background: #08080d; }
/* Each section sticks at the top and the next one scrolls up over it. The
card underneath never moves — it is covered, not pushed — which is what
makes the stack read as depth instead of as a list. */
.sec {
position: sticky; top: 0;
height: 100%;
display: grid; place-items: center;
border-radius: 18px 18px 0 0;
overflow: hidden;
/* Every section needs its own paint layer or the sticky ones bleed into
each other's rounded corners. */
isolation: isolate;
}
.sec .inner { text-align: center; color: #fff; padding: 0 8%; }
.sec h2 { font-size: clamp(24px, 5.4vw, 46px); font-weight: 700; letter-spacing: -0.03em; }
.sec p { margin-top: 10px; font-size: clamp(12px, 1.8vw, 15px); color: #ffffffa8; }
.sec .n {
font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
color: #ffffff70; margin-bottom: 12px;
}
/* Dimming + a slight push back on the section being covered. Driven from JS
as --k (0 = fully exposed, 1 = fully covered). */
.sec .shade {
position: absolute; inset: 0; background: #05050a;
opacity: calc(var(--k, 0) * 0.62); pointer-events: none;
}
.sec .inner { transform: scale(calc(1 - var(--k, 0) * 0.06)); }
.p1 { background: radial-gradient(70% 80% at 30% 25%, #7b2ff7, transparent 62%), #12102a; }
.p2 { background: radial-gradient(70% 80% at 70% 30%, #0a4d8c, transparent 62%), #071427; }
.p3 { background: radial-gradient(70% 80% at 45% 70%, #e0620d, transparent 62%), #23120a; }
.p4 { background: radial-gradient(70% 80% at 55% 40%, #00c2a8, transparent 62%), #04211d; }
.cue {
position: fixed; left: 50%; bottom: 12px; transform: translateX(-50%);
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: #ffffff55; pointer-events: none;
}
</style>
<div class="scroller" id="scroller">
<section class="sec p1"><div class="inner"><div class="n">01</div><h2>Sections stack</h2><p>Each one sticks while the next climbs over it.</p></div><div class="shade"></div></section>
<section class="sec p2"><div class="inner"><div class="n">02</div><h2>Nothing is pushed</h2><p>The card underneath stays where it is. It gets covered.</p></div><div class="shade"></div></section>
<section class="sec p3"><div class="inner"><div class="n">03</div><h2>Depth, not a list</h2><p>Dimming and a 6% scale sell the one below as further away.</p></div><div class="shade"></div></section>
<section class="sec p4"><div class="inner"><div class="n">04</div><h2>Last one rests</h2><p>Give the stack a floor so it does not end mid-transition.</p></div><div class="shade"></div></section>
</div>
<div class="cue">scroll ↓</div>
<script>
const scroller = document.getElementById("scroller");
const secs = [...document.querySelectorAll(".sec")];
const update = () => {
const h = scroller.clientHeight;
secs.forEach((sec, i) => {
const next = secs[i + 1];
// How far the next section has climbed over this one, 0 → 1.
const k = next ? Math.min(1, Math.max(0, 1 - (next.getBoundingClientRect().top / h))) : 0;
sec.style.setProperty("--k", k.toFixed(3));
});
};
scroller.addEventListener("scroll", update, { passive: true });
addEventListener("resize", update);
update();
</script>会翻车的地方
- The video this came from cuts to black right after the title card, so unlike the other six entries here this sample is a reconstruction of the named pattern rather than a match to observed frames. The mechanism is the standard one; the exact numbers are mine.
- Sticky dies silently inside any ancestor with
overflow: hidden. The sections simply scroll away as normal and there is no error anywhere — always the first thing to check when a stack does not stack. - A stack of N sections costs N × 100vh of scrolling to get through. Four is a section; ten is a hostage situation.
- Sticky elements are repainted on every scroll frame. Keep what is inside them cheap — a stack of four sections each containing a video is a very different proposition from four gradients.
出处
@菜心视觉设计 (Douyin) · title card only, the recording ends before the demo · 0:55