SAMPLE

慣性スクロール(Lenis)

Written by naganoma

INDEX

説明

ページスクロール時の動きを滑らかにし、スクロール後の動きに余韻をもたせる方法について解説します。

使用するライブラリ

Lenis(GitHub / 公式サイト

※執筆時点でVer. 1.2.3の情報です。

サンプル

デモページはこちら

手順

1. インストール

まずはLenisをプロジェクト上にインストールします。

npm i lenis

npmなどのパッケージマネージャーを使用しない場合はCDNでの読み込みも可能です。

<script src="https://unpkg.com/[email protected]/dist/lenis.min.js"></script> 

2. CSSの追加

公式が準備しているCSSファイルをプロジェクトに追加します。

@import 'lenis/dist/lenis.css';

npmなどのパッケージマネージャーを使用しない場合はCDNでの読み込みも可能です。

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/lenis.css">

3. セットアップ

JavaScriptでLenisインスタンスの作成を行います。
初期化時にはrequestAnimationFrameを使用してスクロールアニメーションの処理を再帰的に呼び出す必要があります。
なお、呼び出し方法については以下の3パターンがあります。

例1. autoRafオプションを使用する場合

// 初期化
const lenis = new Lenis({
  autoRaf: true, // requestAnimationFrameでスクロール処理を継続的に更新するかどうか
});

例2. rafメソッドを呼び出す場合

autoRafオプションを使用せずにrequestAnimationFrame内でスクロールアニメーションの処理を直接呼び出す方法もあります。

// 初期化
const lenis = new Lenis()

// requestAnimationFrameでスクロール処理を継続的に更新
function raf(time) {
  lenis.raf(time)
  requestAnimationFrame(raf)
}

requestAnimationFrame(raf)

例3. GSAP ScrollTriggerと統合する場合

プロジェクト上でGSAP ScrollTriggerを使用している場合、再帰呼び出し時にGSAPアニメーションの処理とLenisの処理が衝突する懸念があります。
よって、GSAPとLenisの処理を統合するには、GSAP独自のrequestAnimationFrameシステム(gsap.ticker)上でLenisの処理を呼び出すことが推奨されています。

また、GSAPではレンダリング時に、ある一定時間のラグ(デフォルトで500ミリ秒)が発生した際にアニメーションを自動的に抑制する機能が含まれていますが、
これによりLenisのスクロール処理に遅延が発生する懸念があるため、この機能も合わせて無効化しておきます。

// 初期化
const lenis = new Lenis();

// Lenisのスクロールイベントに合わせて全てのScrollTriggerインスタンスをアップデート
lenis.on('scroll', ScrollTrigger.update);

// LenisのrafメソッドをGSAP tickerに追加 
gsap.ticker.add((time) => {
  lenis.raf(time * 1000); // tickerのタイムスタンプの単位は秒なので、ミリ秒に変換
});

// レンダリング時にラグが発生した際のアニメーション調整機能を無効化
gsap.ticker.lagSmoothing(0);

手順3までの処理で基本的な設定は完了です🎉
ページ上で慣性スクロールが適用されていることを確認してみてください。

設定オプション

慣性スクロールの適用対象や滑らかさなどの設定は、インスタンス生成時に特定のオプションを渡すことで調整可能です。
設定オプションについては以下の通りです。

オプション

説明

wrapper

HTMLElement, Window

スクロールコンテナとして使用される要素

content

HTMLElement

スクロールされるコンテンツを含む要素
(通常はwrapperの直接の子要素)

eventsTarget

HTMLElement, Window

 wheeltouchイベントをリッスンする要素

smoothWheel

boolean

wheelイベントによって開始されるスクロールをスムーズにするかどうか

lerp

number

線形補間強度(0~1)

duration

number

スクロールアニメーションの継続時間(秒)

easing

function

スクロールアニメーションに使用するイージング関数
カスタム関数およびEasings.netから選択可能

orientation

string

スクロールの方向
verticalhorizontal または both

gestureOrientation

string

スクロールやスワイプなどのジェスチャー入力の方向
verticalhorizontal または both

syncTouch

boolean

タッチデバイスのスクロールを制御するかどうか
※iOS<16 では不安定になる可能性あり

syncTouchLerp

number

syncTouch中に適用するlerpの値

touchInertiaMultiplier

number

syncTouchによる慣性の強さ

wheelMultiplier

number

マウスホイールによるスクロール速度の設定に使用する乗数

touchMultiplier

number

タッチイベントによるスクロール速度の設定に使用する乗数

infinite

boolean

無限スクロールを有効にするか
※タッチデバイスの場合はsyncTouch: trueが必要

autoResize

boolean

リサイズ時にインスタンスのサイズを自動的に変更するかどうか
 ※falseの場合、別途 .resize()メソッドを呼び出す必要あり

prevent

function

スムーススクロールを特定の要素に対して無効にする
スクロールイベントが通過する要素(node)を引数として受け取り、関数からtrueが返されると、その要素のスムーススクロールが無効化される

virtualScroll

function

スクロールイベントが処理される前に、任意の処理を実行

overscroll

boolean

CSSのoverscroll-behaviorと同様に、ネストされたLenisインスタンスでオーバースクロールを有効にするかどうか

autoRaf

boolean

requestAnimationFrameループを自動的に実行するかどうか

anchors

boolean, ScrollToOptions

アンカーリンクを有効にするかどうか。
scrollToメソッドのオプションを渡すことでスクロールマージンやイージングなどの設定が可能

例えばスクロールの重たさ(速さ)を調整する場合、lerpdurationを変更することで微調整が可能です。
(※ただし、durationeasinglerpとは共存しません)

const lenis = new Lenis({ 
 lerp: 0.6,
})

/* または */
const lenis = new Lenis({ 
 duration: 1,
 easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
})

値を適宜調整しながら、プロジェクトに適したアニメーションを模索してみてください。

注意事項

Lenisは通常のスクロール動作をJavaScriptで上書きするため、スクロールが関係するUI全般に影響を与えます。
場合によってはユーザビリティが低下したり、特定のUIが操作不能になったりするなどの弊害が生まれることもあります。
ここでは代表的な落とし穴と解決方法について説明します。

アンカーリンク(スムーススクロール)の実装

アンカーリンク(<a href="#*">)を実装する際、一般的にはクリック時に対象のセクションまでスムーズにスクロールされる動作(スムーススクロール)が求められます。

実装方法としては主に以下の例が挙げられます。

/* CSSでの実装例 */
html {
 scroll-behavior: smooth;
}

/* JavaScriptでの実装例 */
const anchorlink = document.querySelectorAll('a[href^="#"]') 
anchorlink.forEach((link) => { 
 link.addEventListener('click', (e) => { 
  e.preventDefault()
  const target = document.querySelector(link.hash)
  if (target) { 
   target.scrollIntoView({ 
    block: 'start', 
    behavior: 'smooth',
   })
  } 
 })
})

behavior: smoothはブラウザエンジンがスクロールアニメーションの計算を行うため、よりシンプルなコードで実装できます。
ただし、Lenisを導入している場合はJavaScriptがスクロールアニメーションの計算を行うため、上記の方法で実装するとアニメーションの衝突が発生し、スムーススクロール時にガタつきやジャンプなどが発生してしまいます。
よって、Lenisを導入するプロジェクトではbehavior: smoothでのスムーススクロールの実装は推奨されていません。

スムーススクロールを実装する際はLenisの機能を使用します。
実装方法は主に以下の2パターンがあります。

例1. anchorsオプションを使用する場合

const lenis = new Lenis({ 
 anchors: true
})

/* ScrollToオプションを使用する場合 */
const lenis = new Lenis({ 
 anchors: {
  offset: -80,
  duration: 1,
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
  lock: true
 }
})

インスタンス生成時にanchorsオプションを設定することで、アンカーリンクのスクロールアニメーションが適用されるようになります。

anchorsオプションはブール値またはScrollToOptionsを渡します。
ScrollToOptionsの内訳は以下の通りです。

プロパティ

説明

offset

number

scroll-padding-topに該当する値

lerp

number

線形補間強度(0~1)

duration

number

スクロールアニメーションの継続時間(秒)

easing

function

スクロールアニメーションに使用するイージング関数

immediate

boolean

アニメーション(durationeasinglerp)を無視してスクロールさせるかどうか

lock

boolean

ターゲットに到達するまでスクロールできないようにするかどうか

force

boolean

インスタンスが停止状態でも強制的に実行させるかどうか

onComplete

function

ターゲットに到達した時に呼び出すコールバック

userData

object

scrollイベントと一緒に転送されるデータ

例2. scrollToメソッドを使用する場合

const lenis = new Lenis()
const anchorlink = document.querySelectorAll('a[href^="#"]')

anchorlink.forEach((link) => {
 link.addEventListener('click', (e) => {
  e.preventDefault()
  const target = document.querySelector(link.hash)
  if (target) {
   lenis.scrollTo(target, {
    offset: -80,
    duration: 1,
    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    lock: true
   })
  }
 })
})

anchorsオプションを使用せずにLenisのscrollToメソッドを直接呼び出す方法もあります。
scrollToメソッドを呼び出す際はスクロール先の要素(target)とオプション(ScrollToOptions)を渡します。
ScrollToOptionsの設定項目はanchorsオプションと同様ですが、こちらの手法はtarget要素を変更できるなど拡張性が高い点が特徴です。

スクロールのネスト

実装を進める上で、モーダルウィンドウやドロワーメニューなど、メインコンテンツとは別に、特定の要素内でのみスクロールさせたいケースがあります。
ただし、Lenisを導入している場合、スクロールコンテナ(wrapperオプション)内の要素全てのスクロールがJavaScriptで上書きされるため、特定の要素内でスクロールさせようとしても、メインコンテンツにスクロールが伝播してしまい、要素内でスクロール動作を独立させることができません。

特定の要素内でスクロール動作を独立させるには以下の2つの方法があります。

例1. preventオプションを使用する方法

<div id="modal">スクロール可能コンテンツ</div>
const lenis = new Lenis({ 
 prevent: (node) => node.id === 'modal'
})

インスタンス生成時にpreventオプションを設定することで、特定の要素に対してスクロール動作を独立させることができます。
preventオプションでは、引数にスクロールコンテナ内に存在する要素(node)を受け取り、関数がtrueを返すと、その要素に対してスクロールアニメーションが無効化されます。

例2. Lenisのdata属性を使用する方法

Lenisではスクロールのネスト処理を簡易的に実装できるよう、専用のdata属性が準備されています。
例えば、例1で紹介した処理は以下の方法でも実装できます。

<div data-lenis-prevent>スクロール可能コンテンツ</div>

ホイールイベントやタッチイベントといった特定のイベントのみ無効化したい場合は以下のように記述します。

<!-- ホイールイベントのみ無効化 -->
<div data-lenis-prevent-wheel>スクロール可能コンテンツ</div>

<!-- タッチイベントのみ無効化 -->
<div data-lenis-prevent-touch>スクロール可能コンテンツ</div>

応用(スクロール方向の検知)

Lenisでは、慣性スクロール以外のスクロール関連の処理についても簡易的に実装できます。
今回はデモページでも実装している「下方向にスクロールした際にヘッダーを非表示にし、上方向にスクロールした際にヘッダーを表示」する処理を追加してみます。

const lenis = new Lenis()
const header = document.getElementById('header')

/* スクロールイベントの呼び出し */
lenis.on('scroll', ({ direction }) => {
 header.classList.toggle('is-hidden', direction === 1)
})

Lenisのonメソッドを使用してscrollイベントを呼び出し、イベントの引数にスクロール方向(direction)を受け取ります。
direction1が下方向のスクロール、-1が上方向のスクロールを示す値となるため、この値を元に.is-hiddenクラスを追加/削除しています。

scrollイベントではdirection以外にもスクロールに関する情報を引数で受け取れます。
詳細は公式の「Properties」をご参照ください。

参考文献

Lenis – Get smooth or die trying

darkroomengineering/lenis: How smooth scroll should be

gsap.ticker() | GSAP | Docs & Learning