10 CSS Parallax Effects 09 / 10
CSS Zoom-In / Depth Parallax on Scroll
Cinematic camera-zoom illusion: concentric-ring system scales 1× to 3× while background scales 1× to 2× in the opposite direction as you scroll, building a 'falling forward' sensation.
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="plx-09">
<!-- SCENE 1: Zoom in -->
<div class="plx-09__zoom" id="plx09-zoom">
<div class="plx-09__zoom-sticky">
<div class="plx-09__zoom-bg" id="plx09-bg"></div>
<div class="plx-09__zoom-grid" id="plx09-grid"></div>
<div class="plx-09__rings" id="plx09-rings">
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
</div>
<div class="plx-09__dot"></div>
<div class="plx-09__zoom-content" id="plx09-content">
<span class="plx-09__zoom-eyebrow">Scroll-Driven Zoom · CSS Scale</span>
<h1 class="plx-09__zoom-h">GET<br>CLOSER</h1>
<span class="plx-09__zoom-sub" id="plx09-sub">The world expands toward you. A depth illusion built on CSS scale and scroll position — no 3D transforms needed.</span>
</div>
</div>
</div>
<!-- GALLERY -->
<div class="plx-09__gallery">
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g1"></div>
<div class="plx-09__cell-label">Nebula<small>Deep space · scroll depth</small></div>
</div>
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g2"></div>
<div class="plx-09__cell-label">Abyss<small>Ocean floor · scroll depth</small></div>
</div>
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g3"></div>
<div class="plx-09__cell-label">Ember<small>Core heat · scroll depth</small></div>
</div>
</div>
<!-- OUTRO -->
<div class="plx-09__outro" id="plx09-outro">
<div class="plx-09__outro-sticky">
<div class="plx-09__outro-bg" id="plx09-outro-bg"></div>
<div class="plx-09__outro-text" id="plx09-outro-text">
<h2>Depth is a <em>feeling,</em><br>not a fact.</h2>
<p>Your brain reads scale change as three-dimensional approach. The zoom-in effect triggers that ancient spatial instinct — getting closer to something important.</p>
</div>
</div>
</div>
</div> <div class="plx-09">
<!-- SCENE 1: Zoom in -->
<div class="plx-09__zoom" id="plx09-zoom">
<div class="plx-09__zoom-sticky">
<div class="plx-09__zoom-bg" id="plx09-bg"></div>
<div class="plx-09__zoom-grid" id="plx09-grid"></div>
<div class="plx-09__rings" id="plx09-rings">
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
<div class="plx-09__ring"></div>
</div>
<div class="plx-09__dot"></div>
<div class="plx-09__zoom-content" id="plx09-content">
<span class="plx-09__zoom-eyebrow">Scroll-Driven Zoom · CSS Scale</span>
<h1 class="plx-09__zoom-h">GET<br>CLOSER</h1>
<span class="plx-09__zoom-sub" id="plx09-sub">The world expands toward you. A depth illusion built on CSS scale and scroll position — no 3D transforms needed.</span>
</div>
</div>
</div>
<!-- GALLERY -->
<div class="plx-09__gallery">
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g1"></div>
<div class="plx-09__cell-label">Nebula<small>Deep space · scroll depth</small></div>
</div>
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g2"></div>
<div class="plx-09__cell-label">Abyss<small>Ocean floor · scroll depth</small></div>
</div>
<div class="plx-09__cell">
<div class="plx-09__cell-inner" id="plx09-g3"></div>
<div class="plx-09__cell-label">Ember<small>Core heat · scroll depth</small></div>
</div>
</div>
<!-- OUTRO -->
<div class="plx-09__outro" id="plx09-outro">
<div class="plx-09__outro-sticky">
<div class="plx-09__outro-bg" id="plx09-outro-bg"></div>
<div class="plx-09__outro-text" id="plx09-outro-text">
<h2>Depth is a <em>feeling,</em><br>not a fact.</h2>
<p>Your brain reads scale change as three-dimensional approach. The zoom-in effect triggers that ancient spatial instinct — getting closer to something important.</p>
</div>
</div>
</div>
</div>.plx-09, .plx-09 *, .plx-09 *::before, .plx-09 *::after {
box-sizing: border-box; margin: 0; padding: 0;
}
.plx-09 {
--gold: #d4a843;
--ink: #080608;
--off: #f2eee8;
background: var(--ink);
color: var(--off);
font-family: 'Libre Baskerville', serif;
/* overflow-x:hidden was breaking position:sticky on descendants.
Setting any overflow axis to non-visible creates a new scroll
context, making this element the sticky-containment ancestor
instead of body — so the .plx-09__zoom-sticky inner never
pinned, and the visitor saw the zoom scene bg + rings scroll
past as a single block (looked like dead empty scroll between
sections). overflow:clip is the modern equivalent that
prevents horizontal overflow WITHOUT establishing a scroll
context — sticky works correctly underneath. Chrome 90+,
Safari 16+, Firefox 81+ (all evergreen majors). */
overflow-x: clip;
}
/* ═══ SCENE 1: Zoom-through ═══ */
.plx-09__zoom {
position: relative;
height: 400vh;
}
.plx-09__zoom-sticky {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* BG: scales UP (world expanding toward viewer) */
.plx-09__zoom-bg {
position: absolute;
inset: -20%;
will-change: transform;
transform-origin: center center;
background:
radial-gradient(ellipse at 50% 50%, rgba(212,168,67,0.2) 0%, transparent 40%),
conic-gradient(from 0deg at 50% 50%,
#0a0808 0deg, #110a10 60deg, #0a0a12 120deg,
#0c0808 180deg, #100c08 240deg, #080a0c 300deg, #0a0808 360deg);
}
/* Grid: scales UP faster (closer layer) */
.plx-09__zoom-grid {
position: absolute;
inset: -50%;
will-change: transform;
transform-origin: center center;
background-image:
linear-gradient(rgba(212,168,67,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(212,168,67,0.06) 1px, transparent 1px);
background-size: 80px 80px;
}
/* Rings: scale UP most (fly through them) */
.plx-09__rings {
position: absolute;
width: 100vmin; height: 100vmin;
will-change: transform;
transform-origin: center center;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
.plx-09__ring {
position: absolute;
border-radius: 50%;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
border: 1px solid;
}
.plx-09__ring:nth-child(1) { width: 92%; height: 92%; border-color: rgba(212,168,67,0.10); }
.plx-09__ring:nth-child(2) { width: 74%; height: 74%; border-color: rgba(212,168,67,0.14); }
.plx-09__ring:nth-child(3) { width: 56%; height: 56%; border-color: rgba(212,168,67,0.20); }
.plx-09__ring:nth-child(4) { width: 38%; height: 38%; border-color: rgba(212,168,67,0.30); }
.plx-09__ring:nth-child(5) { width: 22%; height: 22%; border-color: rgba(212,168,67,0.45); }
.plx-09__ring:nth-child(6) { width: 8%; height: 8%; border-color: rgba(212,168,67,0.7); background: rgba(212,168,67,0.1); }
/* Gold dot center */
.plx-09__dot {
position: absolute;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--gold);
top: 50%; left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 20px rgba(212,168,67,0.9), 0 0 60px rgba(212,168,67,0.4);
z-index: 4;
}
/* Content: fades and slides in from bottom as zoom progresses */
.plx-09__zoom-content {
position: relative;
z-index: 10;
text-align: center;
padding: 0 40px;
opacity: 0;
transform: translateY(50px);
will-change: opacity, transform;
pointer-events: none;
}
.plx-09__zoom-eyebrow {
font-family: 'Big Shoulders Display', sans-serif;
font-size: 11px;
letter-spacing: 0.4em;
text-transform: uppercase;
color: var(--gold);
display: block;
margin-bottom: 16px;
}
.plx-09__zoom-h {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(56px, 13vw, 170px);
letter-spacing: -0.04em;
line-height: 0.88;
}
.plx-09__zoom-sub {
margin-top: 24px;
font-style: italic;
font-size: 17px;
line-height: 1.7;
opacity: 0;
color: rgba(242,238,232,0.55);
max-width: 420px;
display: block;
will-change: opacity;
}
/* ═══ SCENE 2: Gallery — zoom inside bounded cells ═══ */
.plx-09__gallery {
padding: 100px 40px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1100px;
margin: 0 auto;
}
.plx-09__cell {
position: relative;
aspect-ratio: 3/4;
overflow: hidden;
cursor: default;
}
/* Each cell inner: starts zoomed in (scale 1.3), scrolls to 1.0 as it enters viewport */
.plx-09__cell-inner {
position: absolute;
inset: -15%;
transform-origin: center center;
will-change: transform;
}
.plx-09__cell:nth-child(1) .plx-09__cell-inner {
background: radial-gradient(circle at 40% 40%, #4a2870 0%, #140820 70%);
}
.plx-09__cell:nth-child(2) .plx-09__cell-inner {
background: radial-gradient(circle at 55% 45%, #154060 0%, #04101c 70%);
}
.plx-09__cell:nth-child(3) .plx-09__cell-inner {
background: radial-gradient(circle at 50% 55%, #5a1a08 0%, #180604 70%);
}
.plx-09__cell-inner::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.1) 0%, transparent 55%);
}
.plx-09__cell-label {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 3;
font-family: 'Big Shoulders Display', sans-serif;
font-size: 32px;
font-weight: 900;
letter-spacing: -0.02em;
text-shadow: 0 2px 16px rgba(0,0,0,0.8);
}
.plx-09__cell-label small {
display: block;
font-family: 'Libre Baskerville', serif;
font-size: 11px;
font-weight: 400;
font-style: italic;
letter-spacing: 0.05em;
opacity: 0.5;
margin-top: 4px;
}
/* ═══ SCENE 3: Outro zoom ═══ */
.plx-09__outro {
position: relative;
height: 300vh;
}
.plx-09__outro-sticky {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.plx-09__outro-bg {
position: absolute;
inset: -20%;
will-change: transform;
transform-origin: center center;
background: radial-gradient(ellipse at 50% 50%, #1a0c26 0%, #080608 65%);
}
.plx-09__outro-text {
position: relative;
z-index: 5;
text-align: center;
will-change: transform;
}
.plx-09__outro-text h2 {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(44px, 10vw, 130px);
letter-spacing: -0.04em;
line-height: 0.9;
}
.plx-09__outro-text h2 em {
font-style: italic;
font-family: 'Libre Baskerville', serif;
font-weight: 400;
color: var(--gold);
font-size: 0.65em;
}
.plx-09__outro-text p {
margin-top: 30px;
font-style: italic;
font-size: 15px;
line-height: 1.8;
color: rgba(242,238,232,0.4);
max-width: 380px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 768px) {
.plx-09__gallery { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.plx-09__zoom-bg, .plx-09__zoom-grid, .plx-09__rings,
.plx-09__cell-inner, .plx-09__outro-bg, .plx-09__outro-text { transform: none !important; }
.plx-09__zoom-content { opacity: 1 !important; transform: none !important; }
.plx-09__zoom-sub { opacity: 1 !important; }
} .plx-09, .plx-09 *, .plx-09 *::before, .plx-09 *::after {
box-sizing: border-box; margin: 0; padding: 0;
}
.plx-09 {
--gold: #d4a843;
--ink: #080608;
--off: #f2eee8;
background: var(--ink);
color: var(--off);
font-family: 'Libre Baskerville', serif;
/* overflow-x:hidden was breaking position:sticky on descendants.
Setting any overflow axis to non-visible creates a new scroll
context, making this element the sticky-containment ancestor
instead of body — so the .plx-09__zoom-sticky inner never
pinned, and the visitor saw the zoom scene bg + rings scroll
past as a single block (looked like dead empty scroll between
sections). overflow:clip is the modern equivalent that
prevents horizontal overflow WITHOUT establishing a scroll
context — sticky works correctly underneath. Chrome 90+,
Safari 16+, Firefox 81+ (all evergreen majors). */
overflow-x: clip;
}
/* ═══ SCENE 1: Zoom-through ═══ */
.plx-09__zoom {
position: relative;
height: 400vh;
}
.plx-09__zoom-sticky {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* BG: scales UP (world expanding toward viewer) */
.plx-09__zoom-bg {
position: absolute;
inset: -20%;
will-change: transform;
transform-origin: center center;
background:
radial-gradient(ellipse at 50% 50%, rgba(212,168,67,0.2) 0%, transparent 40%),
conic-gradient(from 0deg at 50% 50%,
#0a0808 0deg, #110a10 60deg, #0a0a12 120deg,
#0c0808 180deg, #100c08 240deg, #080a0c 300deg, #0a0808 360deg);
}
/* Grid: scales UP faster (closer layer) */
.plx-09__zoom-grid {
position: absolute;
inset: -50%;
will-change: transform;
transform-origin: center center;
background-image:
linear-gradient(rgba(212,168,67,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(212,168,67,0.06) 1px, transparent 1px);
background-size: 80px 80px;
}
/* Rings: scale UP most (fly through them) */
.plx-09__rings {
position: absolute;
width: 100vmin; height: 100vmin;
will-change: transform;
transform-origin: center center;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
.plx-09__ring {
position: absolute;
border-radius: 50%;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
border: 1px solid;
}
.plx-09__ring:nth-child(1) { width: 92%; height: 92%; border-color: rgba(212,168,67,0.10); }
.plx-09__ring:nth-child(2) { width: 74%; height: 74%; border-color: rgba(212,168,67,0.14); }
.plx-09__ring:nth-child(3) { width: 56%; height: 56%; border-color: rgba(212,168,67,0.20); }
.plx-09__ring:nth-child(4) { width: 38%; height: 38%; border-color: rgba(212,168,67,0.30); }
.plx-09__ring:nth-child(5) { width: 22%; height: 22%; border-color: rgba(212,168,67,0.45); }
.plx-09__ring:nth-child(6) { width: 8%; height: 8%; border-color: rgba(212,168,67,0.7); background: rgba(212,168,67,0.1); }
/* Gold dot center */
.plx-09__dot {
position: absolute;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--gold);
top: 50%; left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 20px rgba(212,168,67,0.9), 0 0 60px rgba(212,168,67,0.4);
z-index: 4;
}
/* Content: fades and slides in from bottom as zoom progresses */
.plx-09__zoom-content {
position: relative;
z-index: 10;
text-align: center;
padding: 0 40px;
opacity: 0;
transform: translateY(50px);
will-change: opacity, transform;
pointer-events: none;
}
.plx-09__zoom-eyebrow {
font-family: 'Big Shoulders Display', sans-serif;
font-size: 11px;
letter-spacing: 0.4em;
text-transform: uppercase;
color: var(--gold);
display: block;
margin-bottom: 16px;
}
.plx-09__zoom-h {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(56px, 13vw, 170px);
letter-spacing: -0.04em;
line-height: 0.88;
}
.plx-09__zoom-sub {
margin-top: 24px;
font-style: italic;
font-size: 17px;
line-height: 1.7;
opacity: 0;
color: rgba(242,238,232,0.55);
max-width: 420px;
display: block;
will-change: opacity;
}
/* ═══ SCENE 2: Gallery — zoom inside bounded cells ═══ */
.plx-09__gallery {
padding: 100px 40px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1100px;
margin: 0 auto;
}
.plx-09__cell {
position: relative;
aspect-ratio: 3/4;
overflow: hidden;
cursor: default;
}
/* Each cell inner: starts zoomed in (scale 1.3), scrolls to 1.0 as it enters viewport */
.plx-09__cell-inner {
position: absolute;
inset: -15%;
transform-origin: center center;
will-change: transform;
}
.plx-09__cell:nth-child(1) .plx-09__cell-inner {
background: radial-gradient(circle at 40% 40%, #4a2870 0%, #140820 70%);
}
.plx-09__cell:nth-child(2) .plx-09__cell-inner {
background: radial-gradient(circle at 55% 45%, #154060 0%, #04101c 70%);
}
.plx-09__cell:nth-child(3) .plx-09__cell-inner {
background: radial-gradient(circle at 50% 55%, #5a1a08 0%, #180604 70%);
}
.plx-09__cell-inner::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.1) 0%, transparent 55%);
}
.plx-09__cell-label {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 3;
font-family: 'Big Shoulders Display', sans-serif;
font-size: 32px;
font-weight: 900;
letter-spacing: -0.02em;
text-shadow: 0 2px 16px rgba(0,0,0,0.8);
}
.plx-09__cell-label small {
display: block;
font-family: 'Libre Baskerville', serif;
font-size: 11px;
font-weight: 400;
font-style: italic;
letter-spacing: 0.05em;
opacity: 0.5;
margin-top: 4px;
}
/* ═══ SCENE 3: Outro zoom ═══ */
.plx-09__outro {
position: relative;
height: 300vh;
}
.plx-09__outro-sticky {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.plx-09__outro-bg {
position: absolute;
inset: -20%;
will-change: transform;
transform-origin: center center;
background: radial-gradient(ellipse at 50% 50%, #1a0c26 0%, #080608 65%);
}
.plx-09__outro-text {
position: relative;
z-index: 5;
text-align: center;
will-change: transform;
}
.plx-09__outro-text h2 {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(44px, 10vw, 130px);
letter-spacing: -0.04em;
line-height: 0.9;
}
.plx-09__outro-text h2 em {
font-style: italic;
font-family: 'Libre Baskerville', serif;
font-weight: 400;
color: var(--gold);
font-size: 0.65em;
}
.plx-09__outro-text p {
margin-top: 30px;
font-style: italic;
font-size: 15px;
line-height: 1.8;
color: rgba(242,238,232,0.4);
max-width: 380px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 768px) {
.plx-09__gallery { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.plx-09__zoom-bg, .plx-09__zoom-grid, .plx-09__rings,
.plx-09__cell-inner, .plx-09__outro-bg, .plx-09__outro-text { transform: none !important; }
.plx-09__zoom-content { opacity: 1 !important; transform: none !important; }
.plx-09__zoom-sub { opacity: 1 !important; }
}(() => {
const zoomBlock = document.getElementById('plx09-zoom');
const bg = document.getElementById('plx09-bg');
const grid = document.getElementById('plx09-grid');
const rings = document.getElementById('plx09-rings');
const content = document.getElementById('plx09-content');
const sub = document.getElementById('plx09-sub');
const galCells = [
document.getElementById('plx09-g1'),
document.getElementById('plx09-g2'),
document.getElementById('plx09-g3'),
];
const outroBlock = document.getElementById('plx09-outro');
const outroBg = document.getElementById('plx09-outro-bg');
const outroText = document.getElementById('plx09-outro-text');
let ticking = false;
function prog(el) {
if (!el) return 0;
const r = el.getBoundingClientRect();
const h = el.offsetHeight - window.innerHeight;
return h > 0 ? Math.max(0, Math.min(1, -r.top / h)) : 0;
}
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const vh = window.innerHeight;
// ── SCENE 1 ──
const p = prog(zoomBlock);
// bg: scales from 1 → 2.2 (world rushes in)
if (bg) bg.style.transform = `scale(${1 + p * 1.2})`;
// grid: scales faster (closer layer)
if (grid) grid.style.transform = `scale(${1 + p * 1.8})`;
// rings: fly through (scale 1 → 3.5)
if (rings) rings.style.transform = `translate(-50%,-50%) scale(${1 + p * 2.5})`;
// content: fade in after 35% scroll, slide up
const cp = Math.max(0, (p - 0.35) / 0.45);
if (content) {
content.style.opacity = String(Math.min(1, cp * 1.5));
content.style.transform = `translateY(${(1 - Math.min(1, cp)) * 50}px)`;
}
if (sub) sub.style.opacity = String(Math.min(1, Math.max(0, (p - 0.55) / 0.3)));
// ── GALLERY: parallax inside each cell ──
galCells.forEach((cell) => {
if (!cell) return;
const parent = cell.closest('.plx-09__cell');
if (!parent) return;
const r = parent.getBoundingClientRect();
if (r.bottom < -vh || r.top > vh * 2) return;
const centerOffset = (r.top + r.height / 2) - vh / 2;
// Cell comes in zoomed out (1.0) from below, scale up as it centers
const cellProg = Math.max(0, Math.min(1, (vh * 0.6 - r.top) / (vh * 0.8)));
const scaleVal = 1.0 + cellProg * 0.25;
const yShift = centerOffset * 0.1;
cell.style.transform = `scale(${scaleVal}) translateY(${yShift}px)`;
});
// ── OUTRO ──
const op = prog(outroBlock);
if (outroBg) outroBg.style.transform = `scale(${1 + op * 0.5})`;
if (outroText) outroText.style.transform = `scale(${1 + op * 0.06}) translateY(${op * -18}px)`;
ticking = false;
});
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})(); (() => {
const zoomBlock = document.getElementById('plx09-zoom');
const bg = document.getElementById('plx09-bg');
const grid = document.getElementById('plx09-grid');
const rings = document.getElementById('plx09-rings');
const content = document.getElementById('plx09-content');
const sub = document.getElementById('plx09-sub');
const galCells = [
document.getElementById('plx09-g1'),
document.getElementById('plx09-g2'),
document.getElementById('plx09-g3'),
];
const outroBlock = document.getElementById('plx09-outro');
const outroBg = document.getElementById('plx09-outro-bg');
const outroText = document.getElementById('plx09-outro-text');
let ticking = false;
function prog(el) {
if (!el) return 0;
const r = el.getBoundingClientRect();
const h = el.offsetHeight - window.innerHeight;
return h > 0 ? Math.max(0, Math.min(1, -r.top / h)) : 0;
}
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const vh = window.innerHeight;
// ── SCENE 1 ──
const p = prog(zoomBlock);
// bg: scales from 1 → 2.2 (world rushes in)
if (bg) bg.style.transform = `scale(${1 + p * 1.2})`;
// grid: scales faster (closer layer)
if (grid) grid.style.transform = `scale(${1 + p * 1.8})`;
// rings: fly through (scale 1 → 3.5)
if (rings) rings.style.transform = `translate(-50%,-50%) scale(${1 + p * 2.5})`;
// content: fade in after 35% scroll, slide up
const cp = Math.max(0, (p - 0.35) / 0.45);
if (content) {
content.style.opacity = String(Math.min(1, cp * 1.5));
content.style.transform = `translateY(${(1 - Math.min(1, cp)) * 50}px)`;
}
if (sub) sub.style.opacity = String(Math.min(1, Math.max(0, (p - 0.55) / 0.3)));
// ── GALLERY: parallax inside each cell ──
galCells.forEach((cell) => {
if (!cell) return;
const parent = cell.closest('.plx-09__cell');
if (!parent) return;
const r = parent.getBoundingClientRect();
if (r.bottom < -vh || r.top > vh * 2) return;
const centerOffset = (r.top + r.height / 2) - vh / 2;
// Cell comes in zoomed out (1.0) from below, scale up as it centers
const cellProg = Math.max(0, Math.min(1, (vh * 0.6 - r.top) / (vh * 0.8)));
const scaleVal = 1.0 + cellProg * 0.25;
const yShift = centerOffset * 0.1;
cell.style.transform = `scale(${scaleVal}) translateY(${yShift}px)`;
});
// ── OUTRO ──
const op = prog(outroBlock);
if (outroBg) outroBg.style.transform = `scale(${1 + op * 0.5})`;
if (outroText) outroText.style.transform = `scale(${1 + op * 0.06}) translateY(${op * -18}px)`;
ticking = false;
});
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})();More from 10 CSS Parallax Effects
CSS Sticky Parallax SectionsMulti-Scene Parallax ScrollingCSS Parallax Image Grid / GalleryCSS Horizontal Parallax ScrollCSS Parallax Text Overlay EffectCSS Parallax Card Hover EffectCSS Parallax Background Blur TransitionCSS Parallax Hero SectionMulti-layered CSS Parallax Landscape / Illustration
View the full collection →