【JavaScript】ページを自動スクロールさせる方法|requestAnimationFrame・速度制御・一時停止・ループ・アクセシビリティまで解説

ページを自動スクロールさせる機能は、デジタルサイネージ・お知らせテロップ・プレゼンスライド・オートプレイギャラリーなどで使われます。「ただ動かす」だけでなく、速度制御・一時停止・ループ・タブ非アクティブ時の最適化まで実装できると、実務で即使えるクオリティになります。

この記事では setIntervalrequestAnimationFrame の使い分けから始め、AutoScroller クラスの実装、縦横スクロール、要素内スクロール、アクセシビリティ配慮まで体系的に解説します。

スポンサーリンク

自動スクロールの実装方法:2つのアプローチ

方法 メリット デメリット 向いているケース
setInterval シンプルで理解しやすい フレームレートと無関係。タブ非アクティブ時も動き続ける 簡易実装・学習目的
requestAnimationFrame 画面リフレッシュレートに同期。タブ非アクティブ時は自動停止 やや実装が複雑 滑らかな動き・パフォーマンス重視
本番実装には requestAnimationFrame(rAF)を使いましょう。ディスプレイのリフレッシュレート(60fps・120fps)に同期して描画されるため、setInterval よりもガクつきが少なく、タブが非アクティブのときは自動的にスロットリングされてCPU負荷を抑えられます。

setInterval で自動スクロール(シンプル版)

まず仕組みを理解するためのシンプルな実装です。

setInterval による自動スクロール
let scrollInterval = null;

function startAutoScroll(speed = 2) {
  if (scrollInterval) return; // 二重起動防止
  scrollInterval = setInterval(() => {
    window.scrollBy({ top: speed, behavior: 'instant' });

    // ページ末尾に到達したら停止
    const scrolledToBottom =
      window.scrollY + window.innerHeight >= document.body.scrollHeight - 1;
    if (scrolledToBottom) stopAutoScroll();
  }, 16); // 約60fps
}

function stopAutoScroll() {
  clearInterval(scrollInterval);
  scrollInterval = null;
}

// 使用例
startAutoScroll(3); // 1フレームに3px下へスクロール
behavior: "smooth"scrollBy に指定しながら setInterval で連続呼び出しすると、アニメーションが重複してガクつきます。細かく呼び出す場合は behavior: "instant" にして自分でスムーズさを制御しましょう。

requestAnimationFrame による実装(本番推奨)

requestAnimationFrame を使い、速度制御・一時停止・再開・ループをすべて備えた AutoScroller クラスです。

AutoScroller クラス
class AutoScroller {
  #rafId      = null;   // rAF のID(キャンセルに使う)
  #running    = false;  // 実行中フラグ
  #speed      = 1;      // 1フレームのスクロール量(px)
  #direction  = 1;      // 1=下 / -1=上
  #loop       = false;  // 末尾に達したらループするか
  #target     = window; // スクロール対象(window or 要素)

  constructor({ speed = 1, direction = 1, loop = false, target = window } = {}) {
    this.#speed     = speed;
    this.#direction = direction;
    this.#loop      = loop;
    this.#target    = target;
  }

  get isRunning() { return this.#running; }

  /** スクロールを開始する */
  start() {
    if (this.#running) return;
    this.#running = true;
    this.#tick();
  }

  /** スクロールを一時停止する */
  pause() {
    this.#running = false;
    if (this.#rafId !== null) {
      cancelAnimationFrame(this.#rafId);
      this.#rafId = null;
    }
  }

  /** 一時停止から再開する */
  resume() {
    if (this.#running) return;
    this.#running = true;
    this.#tick();
  }

  /** スクロールを停止して位置をリセット */
  stop() {
    this.pause();
    this.#scrollTo(0);
  }

  /** 速度を動的に変更する */
  setSpeed(speed) { this.#speed = speed; }

  // --- private ---

  #tick() {
    if (!this.#running) return;

    const t = this.#target;
    const isWindow = t === window;
    const scrollTop    = isWindow ? window.scrollY          : t.scrollTop;
    const scrollHeight = isWindow ? document.body.scrollHeight : t.scrollHeight;
    const clientHeight = isWindow ? window.innerHeight       : t.clientHeight;
    const maxScroll    = scrollHeight - clientHeight;

    const next = scrollTop + this.#speed * this.#direction;

    if (next >= maxScroll) {
      // 末尾に到達
      if (this.#loop) {
        this.#scrollTo(0); // 先頭に戻ってループ
      } else {
        this.#scrollTo(maxScroll);
        this.#running = false;
        return;
      }
    } else if (next <= 0) {
      // 先頭に到達(方向が上の場合)
      if (this.#loop) {
        this.#scrollTo(maxScroll); // 末尾に戻ってループ
      } else {
        this.#scrollTo(0);
        this.#running = false;
        return;
      }
    } else {
      this.#scrollTo(next);
    }

    this.#rafId = requestAnimationFrame(() => this.#tick());
  }

  #scrollTo(pos) {
    if (this.#target === window) {
      window.scrollTo({ top: pos, behavior: 'instant' });
    } else {
      this.#target.scrollTop = pos;
    }
  }
}
AutoScroller の使用例
// ページ全体を自動スクロール(速度2px/frame、末尾でループ)
const scroller = new AutoScroller({ speed: 2, loop: true });
scroller.start();

// ボタンで一時停止・再開
document.getElementById('pauseBtn').addEventListener('click', () => {
  scroller.isRunning ? scroller.pause() : scroller.resume();
});

// ユーザーが触ったら一時停止
window.addEventListener('wheel',     () => scroller.pause(), { passive: true });
window.addEventListener('touchstart', () => scroller.pause(), { passive: true });
window.addEventListener('keydown',   () => scroller.pause());

タブ非アクティブ時の最適化

requestAnimationFrame はタブが非アクティブのとき自動的にスロットリングされますが、visibilitychange イベントと組み合わせると確実に制御できます。

visibilitychange で自動停止・再開
const scroller = new AutoScroller({ speed: 1, loop: true });
scroller.start();

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    scroller.pause();  // タブが非表示になったら停止
  } else {
    scroller.resume(); // タブに戻ったら再開
  }
});
なぜ重要か:タブが非表示のままスクロールし続けると、ユーザーが戻ってきたときにページ位置がずれていて混乱を招きます。また不要な描画処理を抑えることでバッテリーやCPUの節約にもなります。

横スクロールと要素内スクロール

横スクロール

横スクロール(scrollLeft)
// AutoScroller を横スクロール用に拡張するシンプル版
class HorizontalScroller {
  #rafId  = null;
  #running = false;
  #speed  = 1;
  #target = null;

  constructor(element, speed = 1) {
    this.#target = element;
    this.#speed  = speed;
  }

  start() {
    if (this.#running) return;
    this.#running = true;
    this.#tick();
  }

  pause() {
    this.#running = false;
    if (this.#rafId) { cancelAnimationFrame(this.#rafId); this.#rafId = null; }
  }

  #tick() {
    if (!this.#running) return;
    const el = this.#target;
    const max = el.scrollWidth - el.clientWidth;
    el.scrollLeft = Math.min(el.scrollLeft + this.#speed, max);
    if (el.scrollLeft >= max) { this.#running = false; return; }
    this.#rafId = requestAnimationFrame(() => this.#tick());
  }
}

// 使用例: #scrollContainer を横に自動スクロール
const hScroller = new HorizontalScroller(
  document.getElementById('scrollContainer'), 2
);
hScroller.start();

特定の要素内を自動スクロール

AutoScroller クラスは target オプションに要素を渡すだけで要素内スクロールにも対応しています。

要素内自動スクロール
const container = document.getElementById('newsTicker');

const ticker = new AutoScroller({
  speed:     1,
  loop:      true,
  target:    container,
  direction: 1,
});

ticker.start();

// ホバーで一時停止(ニュースティッカーでよく使うパターン)
container.addEventListener('mouseenter', () => ticker.pause());
container.addEventListener('mouseleave', () => ticker.resume());
HTML(ニュースティッカーの例)
<div id="newsTicker" style="height: 200px; overflow: hidden; position: relative;">
  <ul>
    <li>お知らせ1: 新機能をリリースしました</li>
    <li>お知らせ2: メンテナンスのお知らせ</li>
    <li>お知らせ3: キャンペーン開催中</li>
    <!-- 繰り返し要素を追加してループ感を演出 -->
  </ul>
</div>

折り返しスクロール(バウンスパターン)

末尾に達したら上に戻るのではなく、逆方向に折り返すパターンです。

バウンスオートスクロール
class BounceScroller {
  #rafId     = null;
  #running   = false;
  #speed     = 1;
  #direction = 1; // 1=下向き / -1=上向き

  constructor(speed = 1) { this.#speed = speed; }

  start() {
    if (this.#running) return;
    this.#running = true;
    this.#tick();
  }

  pause() {
    this.#running = false;
    if (this.#rafId) { cancelAnimationFrame(this.#rafId); this.#rafId = null; }
  }

  #tick() {
    if (!this.#running) return;

    const maxScroll = document.body.scrollHeight - window.innerHeight;
    const next = window.scrollY + this.#speed * this.#direction;

    if (next >= maxScroll) {
      window.scrollTo({ top: maxScroll, behavior: 'instant' });
      this.#direction = -1; // 上向きに折り返す
    } else if (next <= 0) {
      window.scrollTo({ top: 0, behavior: 'instant' });
      this.#direction = 1;  // 下向きに折り返す
    } else {
      window.scrollTo({ top: next, behavior: 'instant' });
    }

    this.#rafId = requestAnimationFrame(() => this.#tick());
  }
}

const bouncer = new BounceScroller(2);
bouncer.start();

スクロール速度を動的に変更する

スライダーやボタンでスクロール速度をリアルタイムに変更できる実装です。

速度コントロールUI
const scroller = new AutoScroller({ speed: 1, loop: true });
scroller.start();

// range スライダーで速度を変更
const speedSlider = document.getElementById('speedSlider');
speedSlider.addEventListener('input', (e) => {
  scroller.setSpeed(Number(e.target.value));
});
HTML(速度スライダーの例)
<label for="speedSlider">スクロール速度:
  <input type="range" id="speedSlider" min="0.5" max="10" step="0.5" value="1">
</label>
<button id="pauseBtn">一時停止 / 再開</button>

アクセシビリティへの配慮

自動スクロールは prefers-reduced-motion に対応することがアクセシビリティの基本です。前庭障害を持つユーザーにとって、動きのあるUIが体調不良を引き起こす場合があります。

prefers-reduced-motion への対応
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const scroller = new AutoScroller({ speed: 2, loop: true });

if (!prefersReduced) {
  scroller.start(); // モーションを許可している場合のみ開始
}

// 設定変化も検知する
window.matchMedia('(prefers-reduced-motion: reduce)')
  .addEventListener('change', (e) => {
    if (e.matches) {
      scroller.pause(); // 動きを減らす設定に切り替えたら停止
    }
  });
アクセシビリティの原則(WCAG 2.1):

  • 自動スクロールが5秒以上続く場合、ユーザーが一時停止・停止・非表示できる仕組みを提供する(達成基準 2.2.2)
  • prefers-reduced-motion: reduce を尊重する
  • スクロール中の要素に aria-live を設定する場合は aria-live="off"(スクロール中は読み上げない)とし、停止時に切り替える

よくある質問(FAQ)

QsetInterval と requestAnimationFrame どちらを使うべきですか?
A本番実装では requestAnimationFrame を使います。ディスプレイのリフレッシュレートに同期するため滑らかで、タブが非アクティブのときはスロットリングされてCPU負荷を抑えられます。setInterval は学習・プロトタイプ用途に向いています。
Qユーザーが手動でスクロールしたら自動スクロールを止めるには?
Awheeltouchstartkeydown イベントを検知して pause() を呼ぶのが基本パターンです。ユーザーが操作したら自動スクロールを止め、明示的な操作(ボタンクリックなど)でのみ再開する設計がUX上も推奨されます。
Q特定の要素にスクロールして止まるにはどうすればいいですか?
A自動スクロール中に目標位置(element.getBoundingClientRect().top + window.scrollY)と現在の window.scrollY を比較し、到達したら停止する処理を #tick() 内に加えます。特定要素への単純なスクロールは指定位置までスクロールする方法(scrollTo・scrollIntoView)の方が適しています。
Qスクロール速度の単位は何ですか?フレームに依存しますか?
ArequestAnimationFrame ベースの実装では「1フレームあたりのpx数」が速度の単位です。60fps環境では speed=1 で毎秒60px移動します。120fps環境では2倍の速さになるため、デバイス非依存にしたい場合は timestamp(rAFのコールバック引数)を使って経過時間ベースの計算にします。
Qページ読み込み後に自動スクロールを開始するには?
Awindow.addEventListener("load", () => scroller.start()) で全リソースの読み込み完了後に開始できます。DOM の構築だけ待てばよい場合は DOMContentLoaded を使います。自動スクロール開始前に少し待ちたい場合は setTimeout でディレイをかけます。

まとめ

自動スクロール実装の要点を整理します。

目的 使う手法
シンプルな自動スクロール setInterval + scrollBy
滑らかな自動スクロール(推奨) requestAnimationFrame
一時停止・再開・速度制御 AutoScroller クラス(cancelAnimationFrame)
タブ非アクティブ時に停止 visibilitychange + pause()
ユーザー操作で停止 wheel / touchstart / keydown イベント
折り返しスクロール direction フラグを反転させる
アクセシビリティ対応 prefers-reduced-motion メディアクエリで無効化

特定の位置や要素へのスクロールには指定位置までスクロールする方法(scrollTo・scrollIntoView)を、スクロール位置を監視して要素を表示するアニメーションにはIntersectionObserverでスクロール時に要素を表示する方法もあわせて参照してください。