【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;
  touch-action: none;
  -webkit-overflow-scrolling: touch;
}
.scroll-track {
  display: flex;
  gap: 12px;
  padding-bottom: 6px;
  padding-right: 6px;
  overflow: auto;
  scroll-behavior: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x pan-y;
}
.scroll-track-horizontal {
  flex-direction: row;
  overflow-x: auto;
  overflow-y: hidden;
  max-height: none;
}
.scroll-track-vertical {
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  max-height: 400px;
  max-width: none;
}
.scroll-track-both {
  flex-wrap: wrap;
  max-height: 400px;
}
.scroll-track::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.scroll-track::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.15);
  border-radius: 999px;
}
.scroll-track::-webkit-scrollbar-corner {
  background: transparent;
}
.card {
  display: flex;
  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);
  flex-shrink: 0;
}
.scroll-track-vertical .card {
  width: 100%;
  min-width: auto;
}

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;
    const onPointerDown = function(e) {
      e.preventDefault();
      try {
        container.setPointerCapture(e.pointerId);
      } catch (err) {
        // Safari/iOSでsetPointerCaptureがサポートされていない場合のフォールバック
      }
      isDown = true;
      container.style.cursor = 'grabbing';
      startX = e.clientX || e.touches?.[0]?.clientX || 0;
      startY = e.clientY || e.touches?.[0]?.clientY || 0;
      startScrollLeft = track.scrollLeft;
      startScrollTop = track.scrollTop;
      lastPointerX = startX;
      lastPointerY = startY;
      lastTimestamp = e.timeStamp || Date.now();
      velocityX = velocityY = 0;
      if (momentumFrameId !== null) {
        cancelAnimationFrame(momentumFrameId);
        momentumFrameId = null;
      }
    };
    const onPointerMove = function(e) {
      if (!isDown) return;
      e.preventDefault();
      const clientX = e.clientX || e.touches?.[0]?.clientX || 0;
      const clientY = e.clientY || e.touches?.[0]?.clientY || 0;
      const dx = clientX - startX;
      const dy = clientY - startY;
      // 縦スクロール専用の場合は横方向のスクロールを無効化
      if (!isVertical) {
        track.scrollLeft = startScrollLeft - dx;
      }
      // 横スクロール専用の場合は縦方向のスクロールを無効化
      if (!isHorizontal) {
        track.scrollTop = startScrollTop - dy;
      }
      const dt = e.timeStamp - lastTimestamp;
      if (dt > 0) {
        // 縦スクロール専用の場合は横方向の速度を0に
        if (isVertical) {
          velocityX = 0;
        } else {
          velocityX = (clientX - lastPointerX) / dt;
        }
        // 横スクロール専用の場合は縦方向の速度を0に
        if (isHorizontal) {
          velocityY = 0;
        } else {
          velocityY = (clientY - lastPointerY) / dt;
        }
        lastPointerX = clientX;
        lastPointerY = clientY;
        lastTimestamp = e.timeStamp || Date.now();
      }
    };
    const onPointerUpOrCancel = function(e) {
      if (!isDown) return;
      e.preventDefault();
      isDown = false;
      container.style.cursor = 'grab';
      try {
        if (e.pointerId !== undefined) {
          container.releasePointerCapture(e.pointerId);
        }
      } catch (err) {
        // Safari/iOSでreleasePointerCaptureがサポートされていない場合のフォールバック
      }
      startMomentumScroll();
    };
    const startMomentumScroll = function() {
      const decay = 0.95;
      const minVelocity = 0.01;
      const step = function() {
        // 縦スクロール専用の場合は横方向のスクロールを無効化
        if (!isVertical) {
          track.scrollLeft -= velocityX * 16;
          velocityX *= decay;
          if (
            track.scrollLeft <= 0 ||
            track.scrollLeft >= track.scrollWidth - track.clientWidth
          ) velocityX = 0;
        } else {
          velocityX = 0;
        }
        // 横スクロール専用の場合は縦方向のスクロールを無効化
        if (!isHorizontal) {
          track.scrollTop -= velocityY * 16;
          velocityY *= decay;
          if (
            track.scrollTop <= 0 ||
            track.scrollTop >= track.scrollHeight - track.clientHeight
          ) velocityY = 0;
        } else {
          velocityY = 0;
        }
        if (Math.abs(velocityX) > minVelocity || Math.abs(velocityY) > minVelocity) {
          momentumFrameId = requestAnimationFrame(step);
        } else {
          momentumFrameId = null;
        }
      };
      if (Math.abs(velocityX) > minVelocity || Math.abs(velocityY) > minVelocity)
        momentumFrameId = requestAnimationFrame(step);
    };
    const onKeyDown = function(e) {
      const step = 80;
      // 縦スクロール専用の場合は横方向のキー操作を無効化
      if (!isVertical) {
        if (e.key === 'ArrowRight') { track.scrollBy({ left: step, behavior: 'smooth' }); e.preventDefault(); }
        if (e.key === 'ArrowLeft') { track.scrollBy({ left: -step, behavior: 'smooth' }); e.preventDefault(); }
      }
      // 横スクロール専用の場合は縦方向のキー操作を無効化
      if (!isHorizontal) {
        if (e.key === 'ArrowDown') { track.scrollBy({ top: step, behavior: 'smooth' }); e.preventDefault(); }
        if (e.key === 'ArrowUp') { track.scrollBy({ top: -step, behavior: 'smooth' }); e.preventDefault(); }
      }
    };
    // Pointer Events (主要ブラウザ対応)
    container.addEventListener('pointerdown', onPointerDown, { passive: false });
    window.addEventListener('pointermove', onPointerMove, { passive: false });
    window.addEventListener('pointerup', onPointerUpOrCancel, { passive: false });
    window.addEventListener('pointercancel', onPointerUpOrCancel, { passive: false });
    // タッチイベントのフォールバック (古いiOS/Safari対応)
    container.addEventListener('touchstart', onPointerDown, { passive: false });
    window.addEventListener('touchmove', onPointerMove, { passive: false });
    window.addEventListener('touchend', onPointerUpOrCancel, { passive: false });
    window.addEventListener('touchcancel', onPointerUpOrCancel, { passive: false });
    container.addEventListener('keydown', onKeyDown, false);
  });
});

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

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

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

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

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

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

まとめ

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

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

本記事のポイント

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

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

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

-フロントエンド
-