【PR】を含みます。

フロントエンド

【JavaScript】ドラッグしてスクロールする方法【初心者向けサンプル付き】

JavaScript ドラッグしてスクロールする方法 初心者向けサンプル付き

Webサイトや管理画面で、スクロールのコンテンツを直感的に操作したい場合があります。

マウスやタッチで「掴んでドラッグするだけ」でスクロールできるUIは、ユーザー体験を向上させる便利な機能です。

この記事では、JavaScriptを使ったドラッグスクロールの基本から、慣性スクロールやキーボード対応まで、初心者でも理解できる手順で解説します。

ドラッグスクロールとは?

ドラッグスクロールは、通常のスクロールバーを使わずに、要素をマウスや指で掴んで動かすことでスクロールする仕組みです。

メリットとしては以下があります。

  • 視覚的に直感的で操作しやすい
  • 横スクロール・縦スクロール両方に対応可能
  • スクロールバーを非表示にできる
  • カルーセルや画像ギャラリーに応用可能

JavaScriptでドラッグスクロールを作る方法

ドラッグスクロールの実装には、以下の3つのポイントがあります。

  1. マウスやタッチの動きを取得する
  2. スクロール位置を動かす
  3. 必要に応じて慣性スクロールやキーボード対応を追加する

今回は、最新のPointer Eventsを使った方法でマウス・タッチ両対応にします。

Pointer Eventsを使ったマウス・タッチ両対応

pointerdown/pointermove/pointerupは、マウス、タッチ、ペン操作を統一的に扱えるイベントです。

古いmousedown/touchstartを分ける必要がなく、コードがすっきりします。

  • .draggable-scrollが操作対象コンテナ
  • .scroll-track内にスクロール可能なコンテンツを並べます(横・縦両方向に対応)
  • tabindex="0"でキーボードアクセス可能に

【サンプル】JavaScriptでドラッグスクロール

横スクロールのサンプル

左右にドラッグして横スクロールできます。

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8

縦スクロールのサンプル

上下にドラッグして縦スクロールできます。

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10

【実装コード】ドラッグスクロールを実装する

HTMLの基本構造

横スクロールと縦スクロールの両方に対応できます。クラス名を変更するだけで切り替え可能です。

Copyをクリックするとコピーできます。

横スクロール用

HTML
Copy
<div class="draggable-scroll" role="region" aria-label="ドラッグで横スクロール" tabindex="0">
  <div class="scroll-track scroll-track-horizontal">
    <div class="card">Item 1</div>
    <div class="card">Item 2</div>
    <div class="card">Item 3</div>
    <div class="card">Item 4</div>
    <div class="card">Item 5</div>
    <div class="card">Item 6</div>
    <div class="card">Item 7</div>
    <div class="card">Item 8</div>
  </div>
</div>

縦スクロール用

HTML
Copy
<div class="draggable-scroll" role="region" aria-label="ドラッグで縦スクロール" tabindex="0">
  <div class="scroll-track scroll-track-vertical">
    <div class="card">Item 1</div>
    <div class="card">Item 2</div>
    <div class="card">Item 3</div>
    <div class="card">Item 4</div>
    <div class="card">Item 5</div>
    <div class="card">Item 6</div>
    <div class="card">Item 7</div>
    <div class="card">Item 8</div>
    <div class="card">Item 9</div>
    <div class="card">Item 10</div>
  </div>
</div>

CSSでデザインを整える

Copyをクリックするとコピーできます。

CSS
Copy
.draggable-scroll {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  padding: 12px;
  overflow: hidden;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 8px;
  cursor: grab;
  user-select: none;
  -webkit-overflow-scrolling: touch;
}
.scroll-track {
  display: flex;
  gap: 12px;
  padding-right: 6px;
  padding-bottom: 6px;
  scroll-behavior: auto;
  transform: translateZ(0);
  will-change: scroll-position;
}
@media (hover: hover) and (pointer: fine) {
  .scroll-track {
    overflow: auto;
  }
  .scroll-track::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }
  .scroll-track::-webkit-scrollbar-thumb {
    background: rgba(0, 0, 0, 0.15);
    border-radius: 100vh;
  }
}
/* タッチデバイスのみ非表示 */
@media (hover: none) and (pointer: coarse) {
  .scroll-track {
    overflow: hidden;
    scrollbar-width: none; /* Firefox */
  }
  .scroll-track::-webkit-scrollbar {
    display: none; /* Chrome, Safari, iOS Safari */
  }
}
.scroll-track-horizontal {
  flex-direction: row;
  overflow-x: auto;
  overflow-y: hidden;
  transform: translateZ(0);
  will-change: scroll-position;
}
.scroll-track-vertical {
  flex-direction: column;
  max-height: 400px;
  overflow-x: hidden;
  overflow-y: auto;
  transform: translateZ(0);
  will-change: scroll-position;
}
.card {
  display: flex;
  flex-shrink: 0;
  justify-content: center;
  align-items: center;
  min-width: 220px;
  min-height: 140px;
  height: 140px;
  font-weight: 600;
  background: linear-gradient(180deg, #f8f9fb, #fff);
  border: 1px solid #eee;
  border-radius: 6px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}

JavaScriptでドラッグ操作を実装

Copyをクリックするとコピーできます。

JavaScript
Copy
document.addEventListener('DOMContentLoaded', function() {
  const containers = document.querySelectorAll('.draggable-scroll');
  containers.forEach(function(container) {
    const track = container.querySelector('.scroll-track');
    const isVertical = track.classList.contains('scroll-track-vertical');
    const isHorizontal = track.classList.contains('scroll-track-horizontal');
    let isDown = false;
    let startX = 0;
    let startY = 0;
    let startScrollLeft = 0;
    let startScrollTop = 0;
    let velocityX = 0;
    let velocityY = 0;
    let lastPointerX = 0;
    let lastPointerY = 0;
    let lastTimestamp = 0;
    let momentumFrameId = null;
    let moveX = 0;
    let moveY = 0;
    let isMovingFrame = false;
    const isCoarse = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
    /* --- 慣性スクロール処理 --- */
    const momentumScroll = function() {
      // 速度が十分小さければ終了
      if (Math.abs(velocityX) < 0.01 && Math.abs(velocityY) < 0.01) {
        momentumFrameId = null;
        return;
      }
      // 慣性によるスクロール
      if (!isVertical) {
        track.scrollLeft -= velocityX * 20;
      }
      if (!isHorizontal) {
        track.scrollTop -= velocityY * 20;
      }
      // 端に到達したら停止
      const atLeft = track.scrollLeft <= 0;
      const atRight = track.scrollLeft + track.clientWidth >= track.scrollWidth;
      const atTop = track.scrollTop <= 0;
      const atBottom = track.scrollTop + track.clientHeight >= track.scrollHeight;
      if ((atLeft && velocityX > 0) || (atRight && velocityX < 0)) {
        velocityX = 0;
      }
      if ((atTop && velocityY > 0) || (atBottom && velocityY < 0)) {
        velocityY = 0;
      }
      // 減衰
      velocityX *= 0.95;
      velocityY *= 0.95;
      momentumFrameId = requestAnimationFrame(momentumScroll);
    };
    /* --- Pointer Down --- */
    const onPointerDown = function(e) {
      isDown = true;
      container.style.cursor = 'grabbing';
      const ptX = e.clientX || e.touches?.[0]?.clientX || 0;
      const ptY = e.clientY || e.touches?.[0]?.clientY || 0;
      startX = ptX;
      startY = ptY;
      startScrollLeft = track.scrollLeft;
      startScrollTop = track.scrollTop;
      lastPointerX = ptX;
      lastPointerY = ptY;
      lastTimestamp = e.timeStamp || Date.now();
      velocityX = 0;
      velocityY = 0;
      // 慣性中なら止める
      if (momentumFrameId !== null) {
        cancelAnimationFrame(momentumFrameId);
        momentumFrameId = null;
      }
      if (isCoarse) {
        track.style.overflow = 'hidden';
      }
    };
    /* --- Pointer Move --- */
    const onPointerMove = function(e) {
      if (!isDown) return;
      const ptX = e.clientX || e.touches?.[0]?.clientX || 0;
      const ptY = e.clientY || e.touches?.[0]?.clientY || 0;
      moveX = ptX - startX;
      moveY = ptY - startY;
      // 端判定
      const atLeft = track.scrollLeft <= 0;
      const atRight = track.scrollLeft + track.clientWidth >= track.scrollWidth;
      const atTop = track.scrollTop <= 0;
      const atBottom = track.scrollTop + track.clientHeight >= track.scrollHeight;
      const draggingLeft = moveX > 0;
      const draggingRight = moveX < 0;
      const draggingUp = moveY > 0;
      const draggingDown = moveY < 0;
      let shouldHandOff = false;
      if (isHorizontal) {
        if ((atLeft && draggingLeft) || (atRight && draggingRight)) {
          shouldHandOff = true;
        }
      }
      if (isVertical) {
        if ((atTop && draggingUp) || (atBottom && draggingDown)) {
          shouldHandOff = true;
        }
      }
      if (shouldHandOff) {
        if (isCoarse) {
          track.style.overflow = 'auto';
        }
        return;
      }
      e.preventDefault();
      if (!isMovingFrame) {
        isMovingFrame = true;
        requestAnimationFrame(function() {
          if (!isVertical) {
            track.scrollLeft = startScrollLeft - moveX;
          }
          if (!isHorizontal) {
            track.scrollTop = startScrollTop - moveY;
          }
          isMovingFrame = false;
        });
      }
      // 速度計算
      const dt = e.timeStamp - lastTimestamp;
      if (dt > 0) {
        velocityX = isVertical ? 0 : (ptX - lastPointerX) / dt;
        velocityY = isHorizontal ? 0 : (ptY - lastPointerY) / dt;
      }
      lastPointerX = ptX;
      lastPointerY = ptY;
      lastTimestamp = e.timeStamp || Date.now();
    };
    /* --- Pointer Up --- */
    const onPointerUp = function() {
      if (!isDown) return;
      isDown = false;
      container.style.cursor = 'grab';
      // 慣性開始
      if (momentumFrameId === null) {
        momentumScroll();
      }
    };
    /* --- Keyboard --- */
    const onKeyDown = function(e) {
      const step = 60;
      if (!isVertical) {
        if (e.key === 'ArrowRight') { track.scrollBy({ left: step }); e.preventDefault(); }
        if (e.key === 'ArrowLeft') { track.scrollBy({ left: -step }); e.preventDefault(); }
        if (e.key === 'Home') { track.scrollTo({ left: 0 }); e.preventDefault(); }
        if (e.key === 'End') { track.scrollTo({ left: track.scrollWidth }); e.preventDefault(); }
      }
      if (!isHorizontal) {
        if (e.key === 'ArrowDown') { track.scrollBy({ top: step }); e.preventDefault(); }
        if (e.key === 'ArrowUp') { track.scrollBy({ top: -step }); e.preventDefault(); }
        if (e.key === 'Home') { track.scrollTo({ top: 0 }); e.preventDefault(); }
        if (e.key === 'End') { track.scrollTo({ top: track.scrollHeight }); e.preventDefault(); }
      }
    };
    /* --- Events --- */
    container.addEventListener('pointerdown', onPointerDown);
    window.addEventListener('pointermove', onPointerMove, { passive: false });
    window.addEventListener('pointerup', onPointerUp);
    container.addEventListener('touchstart', onPointerDown, { passive: false });
    window.addEventListener('touchmove', onPointerMove, { passive: false });
    window.addEventListener('touchend', onPointerUp);
    container.addEventListener('keydown', onKeyDown);
  });
});

アクセシビリティとキーボード対応

ドラッグスクロールを実装する際は、アクセシビリティ(操作しやすさ)への配慮も大切です。

マウスやタッチ操作だけでなく、キーボードでも快適に使えるようにしておきましょう。

  • tabindex="0"を指定して、要素にフォーカスできるようにする
  • 矢印キーで上下左右にスクロール操作できるようにする
  • role属性やaria-labelを追加して、スクリーンリーダーでも内容を理解しやすくする

これらを組み合わせることで、すべてのユーザーにとって使いやすいUIを実現できます。

特に企業サイトや公共サービスなどでは、こうしたアクセシビリティ対応が求められることも多いので、早めに意識しておくのがおすすめです。

まとめ

この記事では、JavaScriptでマウス・タッチ両対応のドラッグスクロールを実装する方法を解説しました。

Pointer Eventsを活用すれば、マウスやタッチ操作を統一的に扱えるため、よりシンプルでメンテナンスしやすいコードになります。

本記事のポイント

  • Pointer Eventsでマウス・タッチ操作を共通化
  • scrollLeftscrollTopを直接操作して自然な動きを実現
  • 慣性スクロールを加えると滑らかで直感的な操作感に
  • キーボード操作やアクセシビリティ対応で、誰にでも使いやすいUIに

このドラッグスクロールの仕組みは、横スクロールのカルーセルや画像ギャラリー、カード型レイアウトなど、さまざまなUIで応用可能です。

ユーザー体験を高めるインタラクションとして、ぜひ取り入れてみてください。

-フロントエンド
-