scroll-pinned-services

Файли-джерела
- index.html
section.section-service-2
Бібліотеки
gsapscrolltrigger
Summary
The Services section pins to the viewport while the user scrolls. Three service cards (Branding, Web Design, No-Code Development) crossfade with their matching background photos in step-snapped progress. Below 1200px the pin and timeline are torn down and the cards stack as a regular flow.
HTML structure (minimal)
<section class="section-service-2 overflow-hidden flat-spacing" id="serviceScroll">
<div class="bg-image-list">
<div class="bg-image"><img src="assets/images/section/bg-service-1.jpg" alt=""><div class="img-item"><img src="assets/images/item/overlay.png" alt=""></div></div>
<div class="bg-image"><img src="assets/images/section/bg-service-2.jpg" alt=""></div>
<div class="bg-image"><img src="assets/images/section/bg-service-3.jpg" alt=""></div>
</div>
<div class="container">
<div class="s-header s-header-scroll"><h2 class="text-display-2 effectFade fadeUp">Services</h2></div>
</div>
<div class="container">
<div class="wrap-control position-relative">
<div class="wg-service-2">
<div class="main-image"><div class="image"><img src="assets/images/section/service-1.jpg" alt=""></div>
<div class="action tf-btn-2 cs-pointer"><i class="icon icon-arrow-long-right"></i></div>
</div>
<div class="center">
<h5 class="title">Branding</h5>
<p class="desc">…</p>
<div class="br-line"></div>
<ul class="tf-list vertical">
<li><span class="text-primary">//</span> Brand Strategy</li>
<!-- … -->
</ul>
<a href="#contactScroll" class="tf-btn">START A PROJECT</a>
</div>
<div class="image-2"><img src="assets/images/section/service-mini-1.jpg" alt=""></div>
</div>
<!-- two more .wg-service-2 -->
</div>
</div>
</section>
Key SCSS tokens
.section-service-2 {
position: relative;
.bg-image-list {
position: absolute;
inset: 0;
z-index: 0;
.bg-image {
position: absolute;
inset: 0;
opacity: 0;
&:first-child { opacity: 1; }
img { width: 100%; height: 100%; object-fit: cover; }
}
}
.wg-service-2 {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
align-items: center;
gap: 64px;
& + .wg-service-2 { opacity: 0; }
}
}
Animation logic
// assets/js/gsapAnimation.js — serviceScroll() initDesktop()
const tl = gsap.timeline({ paused: true });
$mainScrolls.each(function (i, el) {
const $main = $(el);
const $next = $mainScrolls.eq(i + 1);
if ($next.length) {
tl.to($main, { opacity: 0, duration: 0.8, ease: 'power2.out' }, '<');
tl.fromTo($next, { scale: 0.95, opacity: 0 }, { scale: 1, opacity: 1, duration: 1, ease: 'power2.out' }, '<');
tl.to($bg.eq(i), { opacity: 0, duration: 1, ease: 'power2.out' }, '<');
tl.fromTo($bg.eq(i + 1), { opacity: 0 }, { opacity: 1, duration: 1, ease: 'power2.out' }, '<');
}
});
stInstance = ScrollTrigger.create({
trigger: $section[0],
start: 'top top',
end: '+=' + (totalSteps * 1000),
pin: true,
onUpdate: (self) => {
const targetStep = Math.round(self.progress * totalSteps);
if (targetStep !== currentStep && !isAnimating) {
isAnimating = true;
currentStep = targetStep;
tl.tweenTo(currentStep * stepLength * tl.duration(), {
duration: 1,
ease: 'power2.inOut',
onComplete: () => { isAnimating = false; },
});
}
},
});
Notable details
onUpdatequantises continuous scroll progress into integer steps and usestl.tweenToso the panels snap to states instead of scrubbing — feels like a slideshow but is driven by raw scroll position.isNavigatingflag inserviceScroll()ignores onUpdate while a nav-anchor jump is animating, preventing the pin from misbehaving when users hit#serviceScroll.- The whole rig calls
destroyDesktop()and exits below 1200px; resize is debounced 300ms so rapid breakpoint crosses don't double-init.
Use when
- Service or feature comparison sections where you want a "lock-and-swap" experience without giving up scroll feel completely.
- When the design needs background photos to match the active card without cross-section jank.
Caveats
- Pin requires
ScrollTrigger.refresh()after fonts and images load — otherwise the start position is wrong on first paint. - The pin reserves
totalSteps * 1000pixels of scroll — long sections push the rest of the page down by ~3000px on desktop. - Mobile builds skip the entire animation; design content with that fallback in mind.