Scroll-driven pinned hero recomposition
The hero stays pinned while scroll takes it apart: the two hands slide away from their touch point, the ring swells, the centred headline empties out and four fragments arrive in the corners — one composition becoming another without a cut.
How it works
- One progress number feeds three curves:
--p(0–0.85) moves the geometry continuously — hands, ring;--p1(0–0.45) empties the centre;--p2(0.35–0.8) fills the corners. The 0.35–0.45 overlap is deliberate: the stage is never empty, the old composition is still fading as the new one lands. - The hands travel along their own diagonals — human down-left, robot up-right — by 150px each plus a 5° roll. Because they start with fingertips touching at the centre, the first few pixels of scroll read as breaking contact, which is the beat the whole thing hangs on. Frame-diffing the source: the gap opens before anything else changes.
- The ring is a blurred conic gradient masked to a band (
mask: radial-gradient66–88%), rising 70px and scaling to 1.45 with--p. It is the only element that grows; everything else moves or fades, so it reads as the camera pulling back. - Fragments are absolutely positioned at the four corners and arrive from 24px outside their edge (
--dyper corner) with opacity, both on--p2. They are in the DOM from the start; nothing is inserted on scroll. - Everything is a transform or opacity on
--p*written by JS; the layout never changes. The scroll handler stores nothing but progress and schedules onerequestAnimationFrame. - The 320% track gives the recomposition about two viewport heights of scroll. Shorter and the hands jump; longer and the reader stops before the corners have arrived.
Numbers
- Scroll distance
- 320% of the viewport
- Geometry
- --p = smoothstep(0…0.85)
- Centre out / corners in
- 0–0.45 · 0.35–0.8
- Hands
- ±150px along their diagonals · roll ±5°
- Ring
- rise 70px · scale 1 → 1.45
- Fragment travel
- 24px from the nearest edge
Prompt
Paste this into any coding model to get the effect from scratch. It states every number explicitly — vague words like “smooth” or “modern” produce a different result every time.
Build a scroll-driven pinned hero recomposition in vanilla HTML/CSS/JS. Scroll container with a 320%-tall track and a sticky stage (height: 100vh, never 100%). On the stage: a nav row; an iridescent ring (conic-gradient through peach, pink, lavender, sky, mint, gold, masked with radial-gradient(circle, transparent 66%, black 70%, black 84%, transparent 88%), blur 9px) centred; two stylised pointing hands as inline SVG (a skin-tone one from the left, a chrome one with seam lines and a joint from the right) positioned so their fingertips touch at the centre; a centred headline 'NeuralKinetics / cybernetics made organic' with a lede bottom-left and three chips bottom-right; and four text fragments absolutely positioned in the four corners. Read progress p from layout (scrollTop / (trackHeight − clientHeight)) and write three custom properties with smoothstep: --p = slice(p, 0, .85), --p1 = slice(p, 0, .45), --p2 = slice(p, .35, .8). In CSS: human hand transform: translate(calc(var(--p) * -150px), calc(var(--p) * 60px)) rotate(calc(var(--p) * -5deg)); robot hand mirrored; ring translateY(calc(var(--p) * -70px)) scale(calc(1 + var(--p) * .45)); headline, lede and chips opacity: calc(1 − var(--p1)) with the headline rising 40px; fragments opacity: var(--p2) and translateY(calc((1 − var(--p2)) * var(--dy))) with --dy 24px for the top corners and −24px for the bottom ones. Passive scroll listener, one requestAnimationFrame per scroll burst, and a small monospace progress readout.Source
The whole file. It is what the sample above is running — copy it into an .html file and it works with nothing else.
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scroll-driven pinned hero recomposition</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; background: #f2f2f4; color: #17171b; }
.scroller { height: 100%; overflow-y: auto; scrollbar-width: thin; }
.track { height: 320%; }
/* 🩸 100vh,不是 100%:.track 高 320%,100% 会把舞台撑成整条轨道那么高 */
.stage { position: sticky; top: 0; height: 100vh; overflow: hidden; background: linear-gradient(#f7f7f9, #e9e9ee); }
.nav { position: absolute; left: 4%; right: 4%; top: 12px; display: flex; align-items: center; gap: 10px; font-size: 8px; color: #17171b; z-index: 5; }
.nav .pill { padding: 4px 9px; border-radius: 99px; background: #17171b; color: #fff; font-weight: 600; }
.nav .ghost { padding: 4px 9px; border-radius: 99px; background: #ffffffb0; }
.nav .end { margin-left: auto; display: flex; align-items: center; gap: 6px; }
/* the iridescent ring: a blurred conic band that rises and swells with p */
.ring {
position: absolute; left: 50%; top: 50%; width: 320px; height: 320px; margin: -160px 0 0 -160px; border-radius: 50%;
background: conic-gradient(from 200deg, #ffb27a, #ff8fb1, #b9a6ff, #8fd0ff, #a8f0d0, #ffd479, #ffb27a);
mask: radial-gradient(circle, #0000 66%, #000 70%, #000 84%, #0000 88%);
-webkit-mask: radial-gradient(circle, #0000 66%, #000 70%, #000 84%, #0000 88%);
filter: blur(9px); opacity: .85;
transform: translateY(calc(var(--p) * -70px)) scale(calc(1 + var(--p) * .45));
}
/* ---------- the two hands ----------
Stylised: a pointing arm each. They start touching at the centre and
scroll pulls them apart along their own diagonals. */
.hand { position: absolute; top: 50%; width: 300px; height: 90px; margin-top: -45px; }
.hand svg { width: 100%; height: 100%; overflow: visible; }
.hand.human { left: 50%; margin-left: -296px; transform: translate(calc(var(--p) * -150px), calc(var(--p) * 60px)) rotate(calc(var(--p) * -5deg)); }
.hand.robot { left: 50%; margin-left: -4px; transform: translate(calc(var(--p) * 150px), calc(var(--p) * -50px)) rotate(calc(var(--p) * 5deg)); }
/* headline in the middle, the four fragments at the corners */
.title { position: absolute; left: 0; right: 0; top: 22%; text-align: center; opacity: calc(1 - var(--p1)); transform: translateY(calc(var(--p1) * -40px)); }
.title h1 { font-size: 22px; font-weight: 600; letter-spacing: -.01em; }
.title p { font-size: 17px; color: #17171b80; } .title p b { color: #17171b; font-weight: 600; }
.lede { position: absolute; left: 4%; bottom: 12%; width: 190px; font-size: 8.5px; line-height: 1.5; color: #17171bb0; opacity: calc(1 - var(--p1)); }
.lede small { display: block; font-size: 6.5px; margin-bottom: 4px; color: #17171b66; }
.chips { position: absolute; right: 4%; bottom: 12%; display: flex; gap: 6px; opacity: calc(1 - var(--p1)); }
.chips span { padding: 4px 8px; border-radius: 99px; background: #ffffffc0; font-size: 7px; }
.frag { position: absolute; width: 190px; opacity: var(--p2); transform: translateY(calc((1 - var(--p2)) * var(--dy))); }
.frag h2 { font-size: 18px; font-weight: 500; line-height: 1.15; letter-spacing: -.01em; }
.frag h2 span { color: #17171b66; }
.frag p { margin-top: 6px; font-size: 8px; line-height: 1.5; color: #17171b99; }
.frag small { display: block; font-size: 6.5px; color: #17171b66; margin-bottom: 4px; }
.tl { left: 4%; top: 12%; --dy: 24px; }
.tr { right: 4%; top: 12%; text-align: right; --dy: 24px; }
.bl { left: 4%; bottom: 10%; --dy: -24px; }
.br { right: 4%; bottom: 10%; text-align: right; --dy: -24px; }
.tabs { position: absolute; left: 4%; right: 4%; bottom: 3%; display: flex; gap: 22px; font-size: 7px; color: #17171b66; opacity: var(--p2); }
.readout { position: absolute; left: 4%; top: 46px; font: 500 10px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; color: #17171b66; }
.readout b { color: #17171b; font-weight: 600; }
.cue { position: absolute; right: 4%; top: 46px; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: #17171b66; opacity: calc(1 - var(--p1)); }
</style>
<div class="scroller" id="scroller">
<div class="track">
<div class="stage" id="stage" style="--p:0;--p1:0;--p2:0">
<div class="nav"><b>◆ NeuralKinetics</b><span class="pill">● Menu</span><span class="ghost">Advanced Bionics</span><span class="ghost">Cognitive AI</span><span class="end"><span class="pill">+</span>Adaptive Systems</span></div>
<div class="ring"></div>
<div class="hand human"><svg viewBox="0 0 300 90" aria-hidden="true">
<defs><linearGradient id="skin" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#dcae90"/><stop offset="1" stop-color="#a86f52"/></linearGradient></defs>
<!-- forearm -->
<path d="M0 24 Q70 16 150 28 L150 68 Q70 78 0 68 Z" fill="url(#skin)"/>
<!-- palm -->
<path d="M148 26 C180 20 214 24 226 38 L226 60 C214 74 180 76 148 68 Z" fill="url(#skin)"/>
<!-- index finger, pointing -->
<path d="M222 34 L286 30 Q296 31 296 38 Q296 45 286 46 L222 50 Z" fill="#d9a98a"/>
<path d="M250 32 L250 48 M270 31 L270 47" stroke="#b9805f" stroke-width="1.2"/>
<!-- folded fingers -->
<path d="M218 50 Q248 50 252 60 Q250 70 232 70 L218 68 Z" fill="#c48f70"/>
<path d="M214 60 Q236 62 238 72 Q234 78 220 76 Z" fill="#b57f62"/>
<!-- thumb -->
<path d="M176 26 Q206 10 232 22 Q236 30 224 34 Q200 30 176 34 Z" fill="#d9a98a"/>
<path d="M0 30 Q70 22 150 32" stroke="#ffffff59" stroke-width="3" fill="none"/>
</svg></div>
<div class="hand robot"><svg viewBox="0 0 300 90" aria-hidden="true">
<defs><linearGradient id="chrome" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset=".45" stop-color="#cfd3da"/><stop offset=".5" stop-color="#8d939c"/><stop offset="1" stop-color="#e6e8ec"/></linearGradient></defs>
<g transform="translate(300 0) scale(-1 1)">
<path d="M0 24 Q70 16 150 28 L150 68 Q70 78 0 68 Z" fill="url(#chrome)"/>
<path d="M148 26 C180 20 214 24 226 38 L226 60 C214 74 180 76 148 68 Z" fill="url(#chrome)"/>
<path d="M222 34 L286 30 Q296 31 296 38 Q296 45 286 46 L222 50 Z" fill="url(#chrome)"/>
<path d="M218 50 Q248 50 252 60 Q250 70 232 70 L218 68 Z" fill="#b8bcc4"/>
<path d="M214 60 Q236 62 238 72 Q234 78 220 76 Z" fill="#9ea3ab"/>
<path d="M176 26 Q206 10 232 22 Q236 30 224 34 Q200 30 176 34 Z" fill="url(#chrome)"/>
<!-- joints and seams: what makes it read as a machine -->
<g fill="none" stroke="#6f757e" stroke-width="1.4"><path d="M150 28 L150 68"/><path d="M226 38 L226 60"/><path d="M250 31 L250 49"/><path d="M270 30 L270 47"/><path d="M60 22 L60 74"/><path d="M110 24 L110 72"/></g>
<circle cx="190" cy="48" r="7" fill="none" stroke="#6f757e" stroke-width="1.4"/><circle cx="190" cy="48" r="2" fill="#6f757e"/>
</g>
</svg></div>
<div class="title"><h1>NeuralKinetics</h1><p>cybernetics <b>made organic</b></p></div>
<div class="lede"><small>Autonomous Dynamics</small>Unifying biological grace with machine intelligence to design the next era of fusion</div>
<div class="chips"><span>Neuromorphic</span><span>AGI</span><span>Cybernetics</span></div>
<div class="frag tl"><h2>Built to<br><span>move with you</span></h2><p>Biological instinct. Adaptive intelligence. A more natural connection between human and machine.</p></div>
<div class="frag tr"><small>01 / Cognitive intelligence</small><h2>Sense.<br>Understand.<br>Adapt.</h2><p>Intelligence that reads the world around it, learns from every interaction, and responds in the moment.</p></div>
<div class="frag bl"><small>The human side of technology</small><p>We work at the intersection of biology and computation. Every system begins with the way people feel, think, and move.</p></div>
<div class="frag br"><small>02 / Adaptive systems</small><h2>Precision.<br><span>With feeling.</span></h2><p>Engineered for strength. Designed for subtlety. Movement that feels natural.</p></div>
<div class="tabs"><span>Bionics</span><span>Cognition</span><span>Perception</span><span>Robotics</span><span>Adaptation</span></div>
<div class="readout">progress <b id="rp">0.000</b></div>
<div class="cue">scroll ↓</div>
</div>
</div>
</div>
<script>
/* One number, three curves. `--p` drives the geometry continuously (hands,
ring); `--p1` (0…0.45) empties the centre; `--p2` (0.35…0.8) fills the
corners. The overlap 0.35–0.45 is deliberate: the hero is never empty. */
const scroller = document.getElementById("scroller"), stage = document.getElementById("stage"), rp = document.getElementById("rp");
const clamp01 = (v) => Math.min(1, Math.max(0, v));
const slice = (p, a, b) => clamp01((p - a) / (b - a));
const smooth = (x) => x * x * (3 - 2 * x);
let raf = 0;
function update() {
raf = 0;
const max = scroller.firstElementChild.offsetHeight - scroller.clientHeight;
const p = max > 0 ? clamp01(scroller.scrollTop / max) : 0;
stage.style.setProperty("--p", smooth(slice(p, 0, 0.85)).toFixed(4));
stage.style.setProperty("--p1", smooth(slice(p, 0, 0.45)).toFixed(4));
stage.style.setProperty("--p2", smooth(slice(p, 0.35, 0.8)).toFixed(4));
rp.textContent = p.toFixed(3);
}
scroller.addEventListener("scroll", () => { if (!raf) raf = requestAnimationFrame(update); }, { passive: true });
update();
</script>Where it breaks
- Two SVG hands each 300px wide with gradients are cheap; two photographic hands as
<img>are not free to transform if they carryfilterormix-blend-mode. Keep the cut-outs as plain images with alpha. - Text that arrives on scroll is unreachable for someone who stopped scrolling early. All four fragments are always in the DOM and readable by assistive tech regardless of
--p2; do not usevisibilityordisplayfor the fade. - Under reduced motion the composition should still resolve — set
--p*from progress with no smoothing and drop the hand roll, rather than freezing the hero on the touch frame.
Seen in
@优卓UX上岸社 (Douyin) · 「UI交互动效描述」第 4 集 · NeuralKinetics · 0:05