Nahorniak Templates
База референсів шаблонів і компонентів
Назад до шаблону · Davies - Personal Portfolio HTML Template
c91

scroll-pinned-services

Процес·Шаблон: Davies - Personal Portfolio HTML Template·Складність анімації: heavy·Адаптивний: Так
scroll-pinned-services

Файли-джерела

  • index.htmlsection.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

  • onUpdate quantises continuous scroll progress into integer steps and uses tl.tweenTo so the panels snap to states instead of scrubbing — feels like a slideshow but is driven by raw scroll position.
  • isNavigating flag in serviceScroll() 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 * 1000 pixels 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.