ページを自動スクロールさせる機能は、デジタルサイネージ・お知らせテロップ・プレゼンスライド・オートプレイギャラリーなどで使われます。「ただ動かす」だけでなく、速度制御・一時停止・ループ・タブ非アクティブ時の最適化まで実装できると、実務で即使えるクオリティになります。
この記事では setInterval と requestAnimationFrame の使い分けから始め、AutoScroller クラスの実装、縦横スクロール、要素内スクロール、アクセシビリティ配慮まで体系的に解説します。
自動スクロールの実装方法:2つのアプローチ
| 方法 | メリット | デメリット | 向いているケース |
|---|---|---|---|
setInterval |
シンプルで理解しやすい | フレームレートと無関係。タブ非アクティブ時も動き続ける | 簡易実装・学習目的 |
requestAnimationFrame |
画面リフレッシュレートに同期。タブ非アクティブ時は自動停止 | やや実装が複雑 | 滑らかな動き・パフォーマンス重視 |
requestAnimationFrame(rAF)を使いましょう。ディスプレイのリフレッシュレート(60fps・120fps)に同期して描画されるため、setInterval よりもガクつきが少なく、タブが非アクティブのときは自動的にスロットリングされてCPU負荷を抑えられます。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 クラスです。
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;
}
}
}
// ページ全体を自動スクロール(速度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 イベントと組み合わせると確実に制御できます。
const scroller = new AutoScroller({ speed: 1, loop: true });
scroller.start();
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
scroller.pause(); // タブが非表示になったら停止
} else {
scroller.resume(); // タブに戻ったら再開
}
});
横スクロールと要素内スクロール
横スクロール
// 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());
<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();
スクロール速度を動的に変更する
スライダーやボタンでスクロール速度をリアルタイムに変更できる実装です。
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));
});
<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が体調不良を引き起こす場合があります。
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(); // 動きを減らす設定に切り替えたら停止
}
});
- 自動スクロールが5秒以上続く場合、ユーザーが一時停止・停止・非表示できる仕組みを提供する(達成基準 2.2.2)
prefers-reduced-motion: reduceを尊重する- スクロール中の要素に
aria-liveを設定する場合はaria-live="off"(スクロール中は読み上げない)とし、停止時に切り替える
よくある質問(FAQ)
setInterval は学習・プロトタイプ用途に向いています。wheel・touchstart・keydown イベントを検知して pause() を呼ぶのが基本パターンです。ユーザーが操作したら自動スクロールを止め、明示的な操作(ボタンクリックなど)でのみ再開する設計がUX上も推奨されます。element.getBoundingClientRect().top + window.scrollY)と現在の window.scrollY を比較し、到達したら停止する処理を #tick() 内に加えます。特定要素への単純なスクロールは指定位置までスクロールする方法(scrollTo・scrollIntoView)の方が適しています。requestAnimationFrame ベースの実装では「1フレームあたりのpx数」が速度の単位です。60fps環境では speed=1 で毎秒60px移動します。120fps環境では2倍の速さになるため、デバイス非依存にしたい場合は timestamp(rAFのコールバック引数)を使って経過時間ベースの計算にします。window.addEventListener("load", () => scroller.start()) で全リソースの読み込み完了後に開始できます。DOM の構築だけ待てばよい場合は DOMContentLoaded を使います。自動スクロール開始前に少し待ちたい場合は setTimeout でディレイをかけます。まとめ
自動スクロール実装の要点を整理します。
| 目的 | 使う手法 |
|---|---|
| シンプルな自動スクロール | setInterval + scrollBy |
| 滑らかな自動スクロール(推奨) | requestAnimationFrame |
| 一時停止・再開・速度制御 | AutoScroller クラス(cancelAnimationFrame) |
| タブ非アクティブ時に停止 | visibilitychange + pause() |
| ユーザー操作で停止 | wheel / touchstart / keydown イベント |
| 折り返しスクロール | direction フラグを反転させる |
| アクセシビリティ対応 | prefers-reduced-motion メディアクエリで無効化 |
特定の位置や要素へのスクロールには指定位置までスクロールする方法(scrollTo・scrollIntoView)を、スクロール位置を監視して要素を表示するアニメーションにはIntersectionObserverでスクロール時に要素を表示する方法もあわせて参照してください。