【PR】を含みます。

フロントエンド

【JavaScript】完全自作スクロールバーの作り方|CSS非依存でデザイン自由自在

JavaScript 完全自作スクロールバーの作り方|CSS非依存でデザイン自由自在

通常、スクロールバーのデザインはCSSでしか変更できませんが、JavaScriptを使えば「完全自作のスクロールバー」を実現できます。

本記事では、標準スクロールバーを非表示にしてホバー時に表示・ドラッグで操作できるカスタムスクロールバースマホでも動作するカスタムスクロールバーをゼロから実装する方法を解説します。

スクロールバーをCSSでカスタマイズする方法は以下の記事で紹介しています。

あわせて読む
CSS スクロールバーのデザインをカスタマイズする方法

【CSS】スクロールバーのデザインをカスタマイズする方法

もくじスクロールバーをカスタマイズできるブラウザ基本的なカスタマイズ方法(Chrome / Edge / Safari)Firefoxでのカスタマイズ方法クロスブラウザ対応の書き方ダークモード+ホバー ...

JavaScriptでスクロールバーを自作するメリット

通常のスクロールバーはブラウザやOSによってデザインが異なり、サイト全体の統一感を損なうことがあります。

JavaScriptでスクロールバーを自作すると、以下のようなメリットがあります。

  • サイトデザインに合わせて完全カスタマイズ可能
  • ホバー表示やアニメーションにも対応
  • ドラッグ・タッチ操作など、より自然な操作感を再現できる

完成イメージ

  • 標準スクロールバーは非表示
  • マウスホバーでフェードイン
  • つまみ(thumb)をドラッグで操作可能
  • スマホでも指で動かせる

【サンプル】自作スクロールバー

自作スクロールバーのデモです。

このように、スクロールコンテナの中に多くのテキストを入れると...

下や右に進むとスクロールバーが機能します。

ホバーするとスクロールバーが表示されます。

さらに、つまみをドラッグして操作も可能です。

スマホでも指で動かせます。

横スクロールテスト用領域(幅800px)

【実装コード】自作スクロールバーを実装する

HTMLの基本構造

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

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をクリックするとコピーできます。

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でスクロールを制御(スマホ対応)

スマホではmousedownmousemoveが使えないため、touchstart,touchmove,touchendを追加します。

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

JavaScript
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デザインに合わせて自由に拡張してみてください。

-フロントエンド
-