Scroll Choreography Technique

Copy-ready GSAP ScrollTrigger and Lenis setup, pinned horizontal travel, scrubbed scenes, and a native CSS scroll-timeline equivalent.

이 자료로 구현하기

원문 주소와 버전이 포함된 요청문입니다. 복사한 뒤 만들 화면과 요구사항을 채워 에이전트에게 전달하세요.

원문 가이드 ENGLISH

Primary role: executable motion technique.

Repository Boundary

Product-layer script and CSS for showcase/ works and consumer pages. The document keeps vertical scroll ownership; this technique never adds animation to reusable Layout pattern CSS. For scene contracts and briefs, use Scroll-driven Story and the Motion Brief.

Reusable Method

1. Load As Enhancement

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.1.13/dist/lenis.min.js" defer></script>
<script type="module" src="app.js"></script>
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
const { gsap, ScrollTrigger, Lenis } = window;
if (gsap && ScrollTrigger && !reduceMotion) {
  gsap.registerPlugin(ScrollTrigger);
  document.documentElement.classList.add("motion"); // CSS hides pre-animation states only under .motion
  startMotion();
}
.motion [data-reveal] { opacity: 0; transform: translateY(24px); }

Initial hidden states live behind the .motion class. With no script, a failed CDN, or reduced motion, the page stays fully visible.

2. One Ticker For Smooth Scroll And Triggers

const lenis = new Lenis({ lerp: 0.11, wheelMultiplier: 0.9 });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);

Driving Lenis from GSAP's ticker keeps pinned scenes in step with the smoothed scroll. Two separate requestAnimationFrame loops make pins jitter.

In-page links should go through Lenis and still move focus:

for (const link of document.querySelectorAll('a[href^="#"]')) {
  link.addEventListener("click", (event) => {
    const target = document.querySelector(link.getAttribute("href"));
    if (!target) return;
    event.preventDefault();
    lenis.scrollTo(target, { duration: 1.4 });
    target.setAttribute("tabindex", "-1");
    target.focus({ preventScroll: true });
  });
}

3. Reveal On Enter

gsap.to("[data-reveal]", {
  opacity: 1, y: 0, duration: 1.1, ease: "expo.out", stagger: 0.12,
  scrollTrigger: { trigger: ".section", start: "top 80%" },
});

4. Scrubbed Scene

Tie a property directly to scroll progress; scrub: true follows exactly, a number adds catch-up smoothing in seconds.

gsap.to(".hero_inner", {
  yPercent: -18, opacity: 0.25, ease: "none",
  scrollTrigger: { trigger: ".hero", start: "top top", end: "bottom top", scrub: true },
});

5. Pinned Horizontal Travel

Vertical scroll moves a horizontal track while the section is pinned. Enable it only where it fits and keep a vertical list otherwise.

const media = gsap.matchMedia();
media.add("(min-width: 64rem)", () => {
  const track = document.querySelector(".schedule_track");
  const distance = () => track.scrollWidth - document.documentElement.clientWidth;
  gsap.to(track, {
    x: () => -distance(), ease: "none",
    scrollTrigger: { trigger: ".schedule", start: "top top", end: () => `+=${distance()}`, pin: true, scrub: 0.6, invalidateOnRefresh: true },
  });
});
.schedule_viewport { overflow: clip; }             /* the track never widens the document */
@media (min-width: 64rem) { .schedule_track { display: flex; inline-size: max-content; } }

Keep focusable controls out of the moving track, or they can receive focus while translated off-screen.

6. Native Alternative: CSS Scroll-Driven Animation

For a single scrubbed property with no library:

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: reveal linear both;
      animation-range: entry 10% cover 35%;
      animation-timeline: view();
    }
  }
}
@keyframes reveal { from { opacity: 0; transform: translateY(40px); } }

Opinionated Guidance

Choose three or four scroll scenes per page and make each one say something. A page where everything fades up on scroll has no choreography, only delay.

Platform-Specific Guidance

Lenis smooths wheel input but leaves native touch scrolling alone by default, which is the right choice on phones. ScrollTrigger.refresh() runs on resize; functions for x and end plus invalidateOnRefresh let pinned distances recompute. CSS animation-timeline is not available in every engine; wrap it in @supports.

Unsupported Absolutes

Durations, easings, and trigger offsets here were tuned for one page. Pinning is not appropriate for content that must be read at the reader's own pace.

Verification Contract

A page using this technique passes node scripts/check-showcase.mjs: no horizontal overflow while pinned, focus order intact with Lenis active, and all text visible under reduced motion. Review the middle-scroll screenshot to confirm the pinned section clears any fixed header.

Source, License, And Attribution

Locally authored technique. GSAP and Lenis are loaded at runtime under their own licenses; no library source is reproduced.

IA Navigation

Parent: Motion. Next: Kinetic Type Technique.