もくじ
通常、スクロールバーのデザインはCSSでしか変更できませんが、JavaScriptを使えば「完全自作のスクロールバー」を実現できます。
本記事では、標準スクロールバーを非表示にしてホバー時に表示・ドラッグで操作できるカスタムスクロールバー、スマホでも動作するカスタムスクロールバーをゼロから実装する方法を解説します。
スクロールバーをCSSでカスタマイズする方法は以下の記事で紹介しています。
-

【CSS】スクロールバーのデザインをカスタマイズする方法
もくじスクロールバーをカスタマイズできるブラウザ基本的なカスタマイズ方法(Chrome / Edge / Safari)Firefoxでのカスタマイズ方法クロスブラウザ対応の書き方ダークモード+ホバー ...
JavaScriptでスクロールバーを自作するメリット
通常のスクロールバーはブラウザやOSによってデザインが異なり、サイト全体の統一感を損なうことがあります。
JavaScriptでスクロールバーを自作すると、以下のようなメリットがあります。
- サイトデザインに合わせて完全カスタマイズ可能
- ホバー表示やアニメーションにも対応
- ドラッグ・タッチ操作など、より自然な操作感を再現できる
完成イメージ
- 標準スクロールバーは非表示
- マウスホバーでフェードイン
- つまみ(thumb)をドラッグで操作可能
- スマホでも指で動かせる
【サンプル】自作スクロールバー
自作スクロールバーのデモです。
このように、スクロールコンテナの中に多くのテキストを入れると...
下や右に進むとスクロールバーが機能します。
ホバーするとスクロールバーが表示されます。
さらに、つまみをドラッグして操作も可能です。
スマホでも指で動かせます。
横スクロールテスト用領域(幅800px)
【実装コード】自作スクロールバーを実装する
HTMLの基本構造
Copyをクリックするとコピーできます。
<div class="scroll-container"> <div class="scroll-content"> <p>自作スクロールバーのデモです。</p> <p>このように、スクロールコンテナの中に多くのテキストを入れると...</p> <p>下や右に進むとスクロールバーが機能します。</p> <p>ホバーするとスクロールバーが表示されます。</p> <p>さらに、つまみをドラッグして操作も可能です。</p> <p>スマホでも指で動かせます。</p> <p class="scroll-content-horizontal">横スクロールテスト用領域(幅800px)</p> </div> <!-- 縦スクロールバー --> <div class="custom-scrollbar vertical"> <div class="custom-thumb"></div> </div> <!-- 横スクロールバー --> <div class="custom-scrollbar horizontal"> <div class="custom-thumb"></div> </div></div>CSSでデザインを整える
Copyをクリックするとコピーできます。
.scroll-container { position: relative; box-sizing: border-box; width: 100%; max-width: 300px; height: 200px; padding: 10px 15px 15px 10px; overflow: hidden; border: 1px solid #ccc; border-radius: 6px;}.scroll-content { width: 100%; height: 100%; padding-right: 10px; padding-bottom: 10px; overflow: scroll; scrollbar-width: none;}.scroll-content-horizontal { width: 800px;}.scroll-content::-webkit-scrollbar { display: none;}.custom-scrollbar { position: absolute; background: rgba(0, 0, 0, 0.05); opacity: 0; transition: opacity 0.3s; pointer-events: none;}/* 縦スクロールバー */.custom-scrollbar.vertical { top: 5px; right: 3px; width: 6px; height: calc(100% - 15px);}/* 横スクロールバー */.custom-scrollbar.horizontal { bottom: 3px; left: 5px; width: calc(100% - 15px); height: 6px;}/* 非表示時 */.custom-scrollbar.hidden { display: none;}.scroll-container:hover .custom-scrollbar { opacity: 1; pointer-events: auto;}.custom-thumb { position: absolute; background: #666; border-radius: 3px; transition: background 0.2s; cursor: grab;}.custom-thumb:hover { background: #444;}.custom-scrollbar.vertical .custom-thumb { width: 100%;}.custom-scrollbar.horizontal .custom-thumb { height: 100%;}/* スマホでは常時表示 */@media (hover: none) and (pointer: coarse) { .custom-scrollbar { opacity: 1; pointer-events: auto; }}JavaScriptでスクロールを制御(スマホ対応)
スマホではmousedownやmousemoveが使えないため、touchstart,touchmove,touchendを追加します。
Copyをクリックするとコピーできます。
document.addEventListener('DOMContentLoaded', function() { const container = document.querySelector('.scroll-container'); const content = container.querySelector('.scroll-content'); const verticalScrollbar = container.querySelector('.custom-scrollbar.vertical'); const horizontalScrollbar = container.querySelector('.custom-scrollbar.horizontal'); const verticalThumb = verticalScrollbar.querySelector('.custom-thumb'); const horizontalThumb = horizontalScrollbar.querySelector('.custom-thumb'); // --- スクロールバー更新処理 --- function updateThumbs() { const contentHeight = content.clientHeight; const contentWidth = content.clientWidth; const scrollHeight = content.scrollHeight; const scrollWidth = content.scrollWidth; const trackHeight = verticalScrollbar.clientHeight; const trackWidth = horizontalScrollbar.clientWidth; // 縦スクロールバー const showVertical = scrollHeight > contentHeight; verticalScrollbar.classList.toggle('hidden', !showVertical); if (showVertical) { const verticalRatio = contentHeight / scrollHeight; const thumbHeight = Math.max(verticalRatio * trackHeight, 30); verticalThumb.style.height = thumbHeight + 'px'; const scrollRatio = content.scrollTop / (scrollHeight - contentHeight); verticalThumb.style.top = scrollRatio * (trackHeight - thumbHeight) + 'px'; } // 横スクロールバー const showHorizontal = scrollWidth > contentWidth; horizontalScrollbar.classList.toggle('hidden', !showHorizontal); if (showHorizontal) { const horizontalRatio = contentWidth / scrollWidth; const thumbWidth = Math.max(horizontalRatio * trackWidth, 30); horizontalThumb.style.width = thumbWidth + 'px'; const scrollRatio = content.scrollLeft / (scrollWidth - contentWidth); horizontalThumb.style.left = scrollRatio * (trackWidth - thumbWidth) + 'px'; } } // --- スクロール更新 --- let rafId; content.addEventListener('scroll', () => { if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(updateThumbs); }); window.addEventListener('resize', updateThumbs); updateThumbs(); // --- ドラッグ共通関数 --- function enableDrag(thumb, axis, track) { let isDragging = false; let startPos, startScroll; function startDrag(clientPos) { isDragging = true; startPos = clientPos; startScroll = axis === 'y' ? content.scrollTop : content.scrollLeft; thumb.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; } function moveDrag(clientPos) { if (!isDragging) return; const delta = clientPos - startPos; if (axis === 'y') { const contentHeight = content.clientHeight; const scrollHeight = content.scrollHeight; const trackHeight = track.clientHeight; const ratio = (scrollHeight - contentHeight) / (trackHeight - thumb.clientHeight); content.scrollTop = startScroll + delta * ratio; } else { const contentWidth = content.clientWidth; const scrollWidth = content.scrollWidth; const trackWidth = track.clientWidth; const ratio = (scrollWidth - contentWidth) / (trackWidth - thumb.clientWidth); content.scrollLeft = startScroll + delta * ratio; } } function endDrag() { isDragging = false; thumb.style.cursor = 'grab'; document.body.style.userSelect = ''; } // --- マウス --- thumb.addEventListener('mousedown', e => startDrag(axis === 'y' ? e.clientY : e.clientX)); document.addEventListener('mousemove', e => moveDrag(axis === 'y' ? e.clientY : e.clientX)); document.addEventListener('mouseup', endDrag); // --- タッチ --- thumb.addEventListener('touchstart', e => startDrag(axis === 'y' ? e.touches[0].clientY : e.touches[0].clientX)); document.addEventListener('touchmove', e => { if (isDragging) e.preventDefault(); moveDrag(axis === 'y' ? e.touches[0].clientY : e.touches[0].clientX); }, { passive: false }); document.addEventListener('touchend', endDrag); } enableDrag(verticalThumb, 'y', verticalScrollbar); enableDrag(horizontalThumb, 'x', horizontalScrollbar);});スマホ対応のポイント
| 課題 | 対策 |
|---|---|
| hoverが使えない | スマホでは.custom-scrollbarを常時表示 |
| マウスイベントが無効 | touchstart,touchmove,touchendを追加 |
| スクロール追従 | scrollイベントでthumb位置を更新 |
| スクロールの遅延 | スクロール領域をネイティブ処理に任せる |
仕組みの解説(ドラッグ操作とは?)
「ドラッグ操作」とは、つまみ(thumb)をマウスや指で掴んで動かし、それに応じてスクロールを制御する仕組みのことです。
| イベント | 内容 |
|---|---|
mousedown/touchstart | つまみを掴んでドラッグ開始 |
mousemove/touchmove | マウスまたは指の動きに合わせてスクロール量を更新 |
mouseup/touchend | 離してドラッグ終了 |
これにより、標準スクロールバーと同じ操作感を再現できます。
まとめ
JavaScriptを使えば、CSSでは制御できない完全自作のスクロールバーを実現できます。
本記事のポイント
- 標準スクロールバーを非表示にしてデザイン統一
- ホバー表示で視認性UP
- PC・スマホどちらでもドラッグ操作に対応
- CSS・JSのみで完結、ライブラリ不要
応用例
- スクロール位置に応じて色を変化
- スクロールアニメーションとの組み合わせ
今回のコードをベースに、あなたのWebデザインに合わせて自由に拡張してみてください。

