慣性スクロールUIの作り方(スワイプ後に“スーッと止まる動き”)【Inertia Scroll UI】

Inertia Scroll UIのアイキャッチ画像

スマホでリストをスクロールしたとき、
指を離しても「スーッ」と動き続けますよね。

あの気持ちいい動きは、ただのスクロールではなく
“慣性(Inertia)”で動いています。

この記事ではこの動きを、JavaScriptだけで再現します。

なお、今回の実装では requestAnimationFrame を使って毎フレーム位置を更新します。
requestAnimationFrame の基本は、先に「requestAnimationFrameの使い方」で整理しています。

1.基本の考え方

慣性スクロールの正体はシンプルです。

👉 「速度を持ったまま動き続ける」

① 速度を計算する

velocity = currentX - previousX
  • フレームごとの差分
  • これが「今の速さ」

② 指を離したあとも使う

ドラッグ終了後も、この速度を使って動かします。

速度やフレームごとの差分を扱う考え方は、deltaTimeの記事でも詳しく整理しています。
今回の慣性スクロールでも、「1フレームごとにどれだけ動いたか」を見るのが重要です。

2.慣性のコアロジック

velocity *= 0.95
position += velocity
  • 0.95 → 摩擦(friction)
  • 徐々に減速して止まる

3.lerpとの違い

  • lerp → 目的地に近づく
  • inertia → 速度で動く

👉 別物です(ここ重要)

lerpは「現在位置から目標位置へなめらかに近づける」考え方です。
今回の inertia は目標位置ではなく、速度を減衰させながら動かす点が違います。
lerpの基本は「lerpの記事」の中でも使っています。

<div class="container" id="container">
  <div class="track" id="track">
    <div class="card">1</div>
    <div class="card">2</div>
    <div class="card">3</div>
    <div class="card">4</div>
    <div class="card">5</div>
  </div>
</div>
.container {
  width: 100%;
  overflow: hidden;
  border: 1px solid #ddd;
  cursor: grab;
}

.track {
  display: flex;
  gap: 16px;
  padding: 20px;
  will-change: transform;
}

.card {
  min-width: 200px;
  height: 120px;
  background: #111;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
}
const container = document.getElementById("container")
const track = document.getElementById("track")

let isDown = false
let startX = 0
let currentX = 0
let prevX = 0

let position = 0
let velocity = 0

const friction = 0.95

container.addEventListener("mousedown", e => {
  isDown = true
  startX = e.clientX
  velocity = 0
})

window.addEventListener("mousemove", e => {
  if (!isDown) return

  const x = e.clientX
  const delta = x - startX

  prevX = currentX
  currentX = delta

  velocity = currentX - prevX

  position += velocity

  track.style.transform = `translateX(${position}px)`
})

window.addEventListener("mouseup", () => {
  isDown = false
})

function animate() {
  if (!isDown) {
    velocity *= friction
    position += velocity

    if (Math.abs(velocity) < 0.1) {
      velocity = 0
    }

    track.style.transform = `translateX(${position}px)`
  }

  requestAnimationFrame(animate)
}

animate()

4.この実装のポイント

① velocityがすべて

  • 速さを記録
  • 離したあとも使う

② frictionで気持ちよさが決まる

  • 0.9 → すぐ止まる
  • 0.98 → 長く滑る

👉 UX調整ポイント

動きの気持ちよさを調整するという意味では、easingとも近い考え方です。
ただし、easingは時間に対する変化のカーブ、frictionは速度の減衰として考えると分かりやすいです。

③ requestAnimationFrameで継続

慣性スクロールは「1回の処理」ではなく、
毎フレーム少しずつ更新することで実現します。

そのために使うのが requestAnimationFrame です。

requestAnimationFrame の基本的な使い方は、こちらの記事で詳しく解説しています。

5.境界処理(重要)

このままだと無限にスクロールできます。

現実的には👇を入れる

position = Math.max(maxLeft, Math.min(position, maxRight))

または

  • 端で止める
  • バウンスさせる

👉 UX設計の分かれ道

スクロール位置を使ってUIを制御する考え方は、Scroll ProgressScroll Triggerでも使っています。
今回の慣性スクロールは「ユーザー操作後の動き」に注目したパターンです。

6.実験:Momentum Scroll Playground

以下を試してください:

  • frictionを0.9〜0.98で変更
  • 強くドラッグ vs 弱くドラッグ
  • 速度の違いを体感

👉 「iOSっぽさ」はここで作れる

7.よくある失敗

  • velocityを更新してない
  • frictionが強すぎる
  • requestAnimationFrameを使っていない

8.まとめ

慣性スクロールの本質はシンプルです。

👉 「速度を持たせて、徐々に減速する」

この考え方は、

  • カルーセル
  • スワイプUI
  • ゲーム

などにも応用できます。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA