もくじ
Webサイトや管理画面で、スクロールのコンテンツを直感的に操作したい場合があります。
マウスやタッチで「掴んでドラッグするだけ」でスクロールできるUIは、ユーザー体験を向上させる便利な機能です。
この記事では、JavaScriptを使ったドラッグスクロールの基本から、慣性スクロールやキーボード対応まで、初心者でも理解できる手順で解説します。
ドラッグスクロールとは?
ドラッグスクロールは、通常のスクロールバーを使わずに、要素をマウスや指で掴んで動かすことでスクロールする仕組みです。
メリットとしては以下があります。
- 視覚的に直感的で操作しやすい
- 横スクロール・縦スクロール両方に対応可能
- スクロールバーを非表示にできる
- カルーセルや画像ギャラリーに応用可能
JavaScriptでドラッグスクロールを作る方法
ドラッグスクロールの実装には、以下の3つのポイントがあります。
- マウスやタッチの動きを取得する
- スクロール位置を動かす
- 必要に応じて慣性スクロールやキーボード対応を追加する
今回は、最新のPointer Eventsを使った方法でマウス・タッチ両対応にします。
Pointer Eventsを使ったマウス・タッチ両対応
pointerdown/pointermove/pointerupは、マウス、タッチ、ペン操作を統一的に扱えるイベントです。
古いmousedown/touchstartを分ける必要がなく、コードがすっきりします。
.draggable-scrollが操作対象コンテナ.scroll-track内にスクロール可能なコンテンツを並べます(横・縦両方向に対応)tabindex="0"でキーボードアクセス可能に
【サンプル】JavaScriptでドラッグスクロール
横スクロールのサンプル
左右にドラッグして横スクロールできます。
縦スクロールのサンプル
上下にドラッグして縦スクロールできます。
【実装コード】ドラッグスクロールを実装する
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>縦スクロール用
<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をクリックするとコピーできます。
.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をクリックするとコピーできます。
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でマウス・タッチ操作を共通化
scrollLeftやscrollTopを直接操作して自然な動きを実現- 慣性スクロールを加えると滑らかで直感的な操作感に
- キーボード操作やアクセシビリティ対応で、誰にでも使いやすいUIに
このドラッグスクロールの仕組みは、横スクロールのカルーセルや画像ギャラリー、カード型レイアウトなど、さまざまなUIで応用可能です。
ユーザー体験を高めるインタラクションとして、ぜひ取り入れてみてください。
