はじめに
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
<main>
<section class="reveal">セクションA</section>
<section class="reveal">セクションB</section>
<section class="reveal">セクションC</section>
</main>
CSS
main {
display: grid;
gap: 48px;
}
.reveal {
opacity: 0;
transform: translateY(16px);
}
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
<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
.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
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対応を先に入れると運用しやすい