Cinematic scroll-based hero zoom with smooth text transition
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
if (!window.gsap || !window.ScrollTrigger) return;
gsap.registerPlugin(ScrollTrigger);
/* =========================================================
ELEMENT REFERENCES
👉 Edit these ONLY if your attributes/classes change
========================================================= */
const triggerEl = document.querySelector("[zoom-wrapper]");
const targetEl = document.querySelector("[zoom-img-wrapper]");
// Background overlays (used for crossfading)
const overlayA = document.querySelector("[zoom-bg-overlay]"); // Visible → hidden
const overlayB = document.querySelector("[zoom-bg-overlay-overlap]"); // Hidden → visible
if (!triggerEl || !targetEl) return;
/* =========================================================
🎛️ MAIN CONFIG — SAFE TO EDIT
These values control the FEEL of the animation
========================================================= */
// How much of the scroll is used for the main size growth
// 0.25 = first 25% of the scroll
const PHASE1_PORTION = 0.25;
// Final container height after expansion
const MAX_HEIGHT = "150vh";
// Final zoom scale (1 = no zoom)
const FINAL_SCALE = 1.1;
/* =========================================================
🎨 OVERLAY TIMING (CROSSFADE)
========================================================= */
// How EARLY the overlay fade starts before phase 1 ends
// Higher value = earlier fade
const OVERLAY_OVERLAP = 0.15;
// How long the overlay fade lasts (softness)
const OVERLAY_DURATION = 0.55;
/* =========================================================
🔍 SCALE TIMING (ZOOM)
========================================================= */
// Zoom starts slightly after overlay begins
const SCALE_OVERLAP = 0.08;
// Duration of the zoom animation
const SCALE_DURATION = 0.45;
/* =========================================================
INTERNAL STATE (DO NOT EDIT)
========================================================= */
let tl = null;
// Capture the element’s starting size (important for resize)
const getStartSize = () => ({
width: targetEl.offsetWidth,
height: targetEl.offsetHeight
});
function killTimeline() {
if (!tl) return;
tl.scrollTrigger && tl.scrollTrigger.kill();
tl.kill();
tl = null;
}
/* =========================================================
BUILD TIMELINE
Rebuilt on refresh + resize for accuracy
========================================================= */
function build() {
killTimeline();
// Clear inline styles from previous builds
gsap.set(targetEl, { clearProps: "width,height,transform" });
const startSize = getStartSize();
// Performance & transform setup
gsap.set(targetEl, {
boxSizing: "border-box",
transformOrigin: "50% 50%",
willChange: "width, height, transform"
});
// Initial overlay states
if (overlayA) gsap.set(overlayA, { opacity: 1, willChange: "opacity" });
if (overlayB) gsap.set(overlayB, { opacity: 0, willChange: "opacity" });
/* =========================================================
SCROLL-DRIVEN TIMELINE
========================================================= */
tl = gsap.timeline({
scrollTrigger: {
trigger: triggerEl,
start: "top top",
end: "bottom top",
scrub: true,
invalidateOnRefresh: true
// markers: true // ← enable for debugging
}
});
/* ---------------------------------------------------------
PHASE 1 — SIZE EXPANSION
Element grows to full width + tall height
--------------------------------------------------------- */
tl.fromTo(
targetEl,
{
width: startSize.width,
height: startSize.height,
scale: 1
},
{
width: "100vw",
height: MAX_HEIGHT,
scale: 1,
ease: "none",
duration: PHASE1_PORTION
},
0
);
/* ---------------------------------------------------------
OVERLAY CROSSFADE
Starts BEFORE phase 1 finishes
--------------------------------------------------------- */
const overlayStart = Math.max(0, PHASE1_PORTION - OVERLAY_OVERLAP);
if (overlayA) {
tl.to(
overlayA,
{ opacity: 0, ease: "none", duration: OVERLAY_DURATION },
overlayStart
);
}
if (overlayB) {
tl.to(
overlayB,
{ opacity: 1, ease: "none", duration: OVERLAY_DURATION },
overlayStart
);
}
/* ---------------------------------------------------------
FINAL ZOOM
Subtle scale for cinematic finish
--------------------------------------------------------- */
const scaleStart = Math.max(0, PHASE1_PORTION - SCALE_OVERLAP);
tl.to(
targetEl,
{
scale: FINAL_SCALE,
ease: "power1.inOut",
duration: SCALE_DURATION
},
scaleStart
);
}
/* =========================================================
INIT + RESPONSIVENESS
========================================================= */
build();
ScrollTrigger.refresh();
// Rebuild timeline before ScrollTrigger recalculates
ScrollTrigger.addEventListener("refreshInit", build);
// Debounced resize handling
let raf;
window.addEventListener("resize", () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => ScrollTrigger.refresh());
});
});
</script>
Scroll-Driven Cinematic Zoom Guide
This script creates a smooth, scroll-synchronized hero zoom effect where an element expands to full width and height, crossfades background overlays, and finishes with a subtle cinematic scale as the user scrolls through the section.
What this script controls
Scroll behavior overview
Attribute Breakdown & Behavior
[zoom-wrapper]
Purpose:
Acts as the scroll trigger and animation boundary.
Behavior:
[zoom-img-wrapper]
Purpose:
The main element being animated (image or content container).
Behavior on scroll:
100vw)150vh)Why this exists:
Creates a bold, immersive hero transformation without layout jumps.
[zoom-bg-overlay]
Purpose:
Primary background overlay (initial state).
Behavior on scroll:
Why this exists:
Allows a soft visual transition instead of an abrupt background change.
[zoom-bg-overlay-overlap]
Purpose:
Secondary background overlay (overlapping state).
Behavior on scroll:
Why this exists:
Creates a seamless crossfade between visual states.
Scroll timing logic
All animations use scrubbed timing to ensure:
Customization tips
MAX_HEIGHTPHASE1_PORTIONOVERLAY_OVERLAPFINAL_SCALEmarkers: true in ScrollTrigger
Join 1000+ developers getting weekly UI inspiration. No spam, ever.