Lenis×GSAP ScrollTriggerでスムーズスクロールとアニメーションを実装

はじめに

Lenis は、ホイールやタッチ操作に対して自然な補間を加え、ページ全体のスクロール体験を整えるライブラリです。移動の質感を担当させると、画面全体の印象を崩さずにチューニングできます。

GSAP は JavaScript アニメーションの定番ライブラリで、ScrollTrigger はその中でもスクロール位置とアニメーションの開始・進行を結び付けるプラグインです。入場演出だけでなく、進捗バー、パララックス、固定表示との連動まで同じ書き味で扱えます。

この組み合わせで押さえておきたいのは、次の役割分担です。

  • Lenis はスクロールの滑らかさを担当する
  • GSAP は要素アニメーションの見せ方を担当する
  • ScrollTrigger は「いつ動かすか」「どこまで進めるか」を担当する

インストール方法

JavaScript

npm i lenis gsap

ScrollTrigger は GSAP 本体に含まれているため、追加パッケージは不要です。Lenis は推奨CSSも用意されているので、最初に読み込んでおくと挙動が安定しやすくなります。

JavaScript

import Lenis from "lenis"
import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import "lenis/dist/lenis.css"

gsap.registerPlugin(ScrollTrigger)

基本の使い方

まずは、セクションが画面内に入ったら自然に表示する最小例です。Lenis と ScrollTrigger を組み合わせるときは、Lenis のスクロール更新を ScrollTrigger 側へ渡すのが最初のポイントになります。

See the Pen lenis-motion by watanabe (@web-sourcecode) on CodePen.

HTML

HTML
<main>
  <section class="reveal">セクションA</section>
  <section class="reveal">セクションB</section>
  <section class="reveal">セクションC</section>
</main>

CSS

CSS
main {
  display: grid;
  gap: 48px;
}

.reveal {
  opacity: 0;
  transform: translateY(16px);
}

JavaScript

JavaScript
import Lenis from "lenis"
import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import "lenis/dist/lenis.css"

gsap.registerPlugin(ScrollTrigger)

const lenis = new Lenis({
  duration: 1.1,
  smoothWheel: true
})

lenis.on("scroll", ScrollTrigger.update)

gsap.ticker.add((time) => {
  lenis.raf(time * 1000)
})

gsap.ticker.lagSmoothing(0)

gsap.utils.toArray(".reveal").forEach((element) => {
  gsap.to(element, {
    opacity: 1,
    y: 0,
    duration: 0.5,
    ease: "power2.out",
    scrollTrigger: {
      trigger: element,
      start: "top 80%",
      toggleActions: "play none none reverse"
    }
  })
})

この形なら、Lenis がスクロールの気持ちよさを作りつつ、ScrollTrigger が要素ごとの表示タイミングだけを見ます。処理の責務が分かれているので、後から開始位置や easing を触っても影響範囲を追いやすいです。

Lenis と ScrollTrigger の組み合わせは、単純なフェードアップだけでなく「スクロール量に意味を持たせたいページ」と相性が良いです。たとえば、サービス紹介の章切り替え、採用ページのストーリー表示、導入事例の進捗UI付きレイアウトなどで効いてきます。

特に ScrollTrigger を選ぶ価値が出やすいのは、入場演出より一歩進んだパターンです。scrub でスクロール量に合わせてビジュアルを少し動かしたり、進捗バーや固定ナビと連動させたりするときは、設計を一箇所に寄せやすくなります。

応用コード

次は、読み進め量に応じた進捗バーと、hero ビジュアルの軽い scrub 演出を追加します。動きが増えるので、prefers-reduced-motion の分岐も最初から入れておきます。

See the Pen Lenis×GSAP ScrollTrigger 2 by watanabe (@web-sourcecode) on CodePen.

HTML

HTML
<header class="site-header">
  <div class="reading-progress"><span id="js-progress"></span></div>
</header>

<main id="js-content">
  <section class="hero">
    <div class="hero__media" id="js-hero-media">Feature Visual</div>
  </section>

  <section class="reveal">セクションA</section>
  <section class="reveal">セクションB</section>
  <section class="reveal">セクションC</section>
</main>

CSS

CSS
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  padding: 12px 16px;
  background: rgb(255 255 255 / 88%);
  backdrop-filter: blur(8px);
}

.reading-progress {
  height: 3px;
  background: rgb(15 23 42 / 10%);
  border-radius: 999px;
  overflow: hidden;
}

.reading-progress span {
  display: block;
  width: 100%;
  height: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: #0ea5e9;
}

.hero {
  min-height: 80vh;
  display: grid;
  place-items: center;
}

.hero__media {
  width: min(720px, 100%);
  min-height: 320px;
  display: grid;
  place-items: center;
  border-radius: 24px;
  background: linear-gradient(135deg, #0f172a, #1d4ed8);
  color: #fff;
}

JavaScript

JavaScript
import Lenis from "lenis"
import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import "lenis/dist/lenis.css"

gsap.registerPlugin(ScrollTrigger)

const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches
const content = document.getElementById("js-content")
const progress = document.getElementById("js-progress")
const heroMedia = document.getElementById("js-hero-media")
const sections = gsap.utils.toArray(".reveal")

const lenis = new Lenis({
  duration: prefersReduced ? 0.01 : 1.05,
  smoothWheel: !prefersReduced
})

lenis.on("scroll", ScrollTrigger.update)

gsap.ticker.add((time) => {
  lenis.raf(time * 1000)
})

gsap.ticker.lagSmoothing(0)

if (prefersReduced) {
  gsap.set(sections, { opacity: 1, y: 0 })
} else {
  sections.forEach((section) => {
    gsap.to(section, {
      opacity: 1,
      y: 0,
      duration: 0.55,
      ease: "power2.out",
      scrollTrigger: {
        trigger: section,
        start: "top 82%",
        toggleActions: "play none none reverse"
      }
    })
  })

  gsap.to(heroMedia, {
    yPercent: -10,
    ease: "none",
    scrollTrigger: {
      trigger: heroMedia,
      start: "top bottom",
      end: "bottom top",
      scrub: true
    }
  })
}

ScrollTrigger.create({
  trigger: content,
  start: "top top",
  end: "bottom bottom",
  onUpdate: (self) => {
    gsap.set(progress, { scaleX: self.progress })
  }
})

window.addEventListener("load", () => {
  ScrollTrigger.refresh()
})

ScrollTrigger 側には「開始位置」「進捗」「scrub」の管理だけを持たせ、Lenis にはスクロール補間だけを任せると見通しが崩れにくいです。画像やWebフォントの読み込み後にレイアウトが動くページでは、最後に ScrollTrigger.refresh() を入れておくとズレを吸収しやすくなります。

注意点

Lenis をデフォルトの window スクロールで使うなら、今回のような連携で十分です。ただし、独自のラッパー要素をスクロールコンテナにする構成では、ScrollTrigger 側でも scroller の設定や scrollerProxy() の検討が必要になります。

また、scrub を多用すると、気持ちよさより「重さ」が先に見えてしまうことがあります。まずは入場演出と進捗UIくらいに絞り、必要な箇所だけ段階的に追加するほうが、読みやすさを守りやすいです。

まとめ

Lenis と GSAP ScrollTrigger を組み合わせると、スクロールの質感と演出の制御をきれいに分業できます。Lenis で移動体験を整え、ScrollTrigger で要素の見せ方や進捗を設計すると、調整ポイントが散らばりにくくなります。

特に、入場演出だけで終わらず、scrub や進捗バーまで視野に入っているページでは扱いやすい構成です。まずは最小例でつなぎ込み、必要な箇所だけ段階的に拡張していく進め方がおすすめです。

ポイント

  • Lenis はスクロールの質感、ScrollTrigger は表示タイミングと進捗管理に分ける
  • Lenis 連携時は lenis.on("scroll", ScrollTrigger.update) と GSAP ticker 同期が基本になる
  • scrub や進捗バーのようなスクロール連動UIで ScrollTrigger の強みが出やすい
  • 画像読み込み後の ScrollTrigger.refresh()prefers-reduced-motion 対応を先に入れると運用しやすい

参考リンク

read next