スライドショーは「画像を自動的に切り替えながらループ再生する」シンプルなものから、前後ボタン・ドットナビ・タッチスワイプ・キーボード操作まで備えた本格的なものまで幅があります。ライブラリを使えば手軽ですが、仕組みを理解して自前実装できると挙動のカスタマイズや軽量化に直結します。
この記事では画像の src を切り替える最小構成から始め、CSS トランジションによるフェード・スライドアニメーション、ナビゲーションボタン、ドットインジケーター、スマホのタッチスワイプ対応まで、段階的に機能を追加して実装します。
最小構成:setInterval で自動ループ
最もシンプルな実装は setInterval() で一定間隔に img.src を切り替えるだけです。
<div class="slideshow"> <img id="slide-img" src="image1.jpg" alt="スライド画像" width="800" height="450"> </div>
const images = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
let current = 0;
const img = document.getElementById('slide-img');
setInterval(() => {
current = (current + 1) % images.length; // インデックスをループ
img.src = images[current];
}, 3000); // 3秒ごとに切り替え
%(剰余演算子)を使うと、インデックスが最後の要素を超えたとき自動的に 0 に戻ります。例: 画像が3枚なら 0→1→2→0→1→2 と繰り返します。if (current >= images.length) current = 0; と同じ意味ですが、1行で書けます。CSS トランジションでフェードアニメーションを付ける
画像が瞬間的に切り替わると視覚的に荒く見えます。CSS の opacity トランジションを使ってスムーズなフェードを実現します。
<div class="slideshow-fade"> <img class="slide-item active" src="image1.jpg" alt="スライド1" width="800" height="450"> <img class="slide-item" src="image2.jpg" alt="スライド2" width="800" height="450"> <img class="slide-item" src="image3.jpg" alt="スライド3" width="800" height="450"> </div>
.slideshow-fade {
position: relative;
width: 800px;
height: 450px;
overflow: hidden;
}
.slideshow-fade .slide-item {
position: absolute;
inset: 0; /* top/right/bottom/left: 0 をまとめた書き方 */
width: 100%;
height: 100%;
object-fit: cover; /* 画像がコンテナにフィットするようトリミング */
opacity: 0;
transition: opacity 0.8s ease;
}
.slideshow-fade .slide-item.active {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.slideshow-fade .slide-item {
transition: none; /* アニメーション無効設定を尊重 */
}
}
const slides = document.querySelectorAll('.slideshow-fade .slide-item');
let current = 0;
function goTo(index) {
slides[current].classList.remove('active');
current = (index + slides.length) % slides.length; // 負のインデックスも安全に処理
slides[current].classList.add('active');
}
// 自動再生
let timer = setInterval(() => goTo(current + 1), 3000);
CSS トランジションでスライドアニメーションを付ける
左右にスライドして切り替わるタイプのアニメーションです。translateX で各スライドの位置を制御します。
<div class="slideshow-slide">
<div class="slide-track">
<div class="slide-item"><img src="image1.jpg" alt="スライド1" width="800" height="450"></div>
<div class="slide-item"><img src="image2.jpg" alt="スライド2" width="800" height="450"></div>
<div class="slide-item"><img src="image3.jpg" alt="スライド3" width="800" height="450"></div>
</div>
</div>
.slideshow-slide {
width: 800px;
height: 450px;
overflow: hidden;
}
.slide-track {
display: flex;
transition: transform 0.5s ease;
}
.slide-track .slide-item {
flex: 0 0 800px; /* 1枚分の幅で固定 */
height: 450px;
}
.slide-track .slide-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
const track = document.querySelector('.slide-track');
const items = document.querySelectorAll('.slide-track .slide-item');
const width = items[0].offsetWidth; // 1枚の幅を取得
let current = 0;
function goTo(index) {
current = (index + items.length) % items.length;
track.style.transform = `translateX(${-current * width}px)`;
}
setInterval(() => goTo(current + 1), 3000);
前後ナビゲーションボタンを追加する
ユーザーが手動で前後に切り替えられる「前へ」「次へ」ボタンを追加します。
<div class="slideshow-nav" role="region" aria-label="画像スライドショー">
<div class="slide-wrapper">
<!-- 前後ボタン -->
<button class="nav-btn prev-btn" aria-label="前のスライド">❮</button>
<button class="nav-btn next-btn" aria-label="次のスライド">❯</button>
<!-- スライド画像 -->
<div class="slide-track">
<div class="slide-item"><img src="image1.jpg" alt="スライド1" width="800" height="450"></div>
<div class="slide-item"><img src="image2.jpg" alt="スライド2" width="800" height="450"></div>
<div class="slide-item"><img src="image3.jpg" alt="スライド3" width="800" height="450"></div>
</div>
</div>
<!-- ドットインジケーター -->
<div class="dot-nav" role="tablist" aria-label="スライド選択"></div>
</div>
.slideshow-nav {
width: 800px;
user-select: none;
}
.slide-wrapper {
position: relative;
height: 450px;
overflow: hidden;
}
/* 前後ボタン */
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(0, 0, 0, 0.45);
color: #fff;
border: none;
padding: 12px 18px;
font-size: 1.2rem;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}
.nav-btn:hover { background: rgba(0, 0, 0, 0.7); }
.nav-btn:focus-visible {
outline: 3px solid #60a5fa;
outline-offset: 2px;
}
.prev-btn { left: 12px; }
.next-btn { right: 12px; }
/* ドットインジケーター */
.dot-nav {
display: flex;
justify-content: center;
gap: 8px;
padding: 12px 0;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #d1d5db;
border: none;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.dot.active { background: #2563eb; transform: scale(1.3); }
.dot:focus-visible { outline: 3px solid #60a5fa; outline-offset: 2px; }
完成形:Slideshow クラス
自動再生・前後ボタン・ドットナビ・ホバー一時停止・キーボード操作・タッチスワイプをすべて備えた完成形クラスです。
class Slideshow {
#slides; // スライド要素の配列
#current = 0;
#timer = null;
#opts;
#root;
/**
* @param {string|Element} root - スライドショーのルート要素(セレクタ or Element)
* @param {object} opts
* @param {number} opts.interval - 自動再生間隔(ミリ秒)
* @param {boolean} opts.autoplay - 自動再生するか
* @param {boolean} opts.loop - 最後のスライド後に最初に戻るか
*/
constructor(root, opts = {}) {
this.#root = typeof root === 'string' ? document.querySelector(root) : root;
this.#opts = { interval: 4000, autoplay: true, loop: true, ...opts };
this.#slides = Array.from(this.#root.querySelectorAll('.slide-item'));
this.#buildDots();
this.#bindEvents();
this.#goTo(0);
if (this.#opts.autoplay) this.#start();
}
// ── ドット生成 ──────────────────────────────
#buildDots() {
const nav = this.#root.querySelector('.dot-nav');
if (!nav) return;
this.#slides.forEach((_, i) => {
const btn = document.createElement('button');
btn.className = 'dot';
btn.setAttribute('role', 'tab');
btn.setAttribute('aria-label', `スライド ${i + 1}`);
btn.addEventListener('click', () => { this.#goTo(i); this.#restart(); });
nav.appendChild(btn);
});
}
// ── スライド移動 ────────────────────────────
#goTo(index) {
const len = this.#slides.length;
if (!this.#opts.loop && (index < 0 || index >= len)) return;
this.#current = ((index % len) + len) % len;
// スライド切り替え
this.#slides.forEach((s, i) => s.classList.toggle('active', i === this.#current));
// ドット更新
this.#root.querySelectorAll('.dot').forEach((d, i) => {
d.classList.toggle('active', i === this.#current);
d.setAttribute('aria-selected', String(i === this.#current));
});
}
// ── タイマー制御 ────────────────────────────
#start() {
this.#timer = setInterval(() => this.#goTo(this.#current + 1), this.#opts.interval);
}
#stop() { clearInterval(this.#timer); this.#timer = null; }
#restart() { this.#stop(); if (this.#opts.autoplay) this.#start(); }
// ── イベント設定 ────────────────────────────
#bindEvents() {
const wrapper = this.#root.querySelector('.slide-wrapper') ?? this.#root;
// 前後ボタン
this.#root.querySelector('.prev-btn')
?.addEventListener('click', () => { this.#goTo(this.#current - 1); this.#restart(); });
this.#root.querySelector('.next-btn')
?.addEventListener('click', () => { this.#goTo(this.#current + 1); this.#restart(); });
// ホバーで一時停止
wrapper.addEventListener('mouseenter', () => this.#stop());
wrapper.addEventListener('mouseleave', () => { if (this.#opts.autoplay) this.#start(); });
// キーボード操作
this.#root.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') { this.#goTo(this.#current - 1); this.#restart(); }
if (e.key === 'ArrowRight') { this.#goTo(this.#current + 1); this.#restart(); }
});
// タッチスワイプ
let touchX = 0;
wrapper.addEventListener('touchstart', (e) => {
touchX = e.touches[0].clientX;
}, { passive: true });
wrapper.addEventListener('touchend', (e) => {
const diff = touchX - e.changedTouches[0].clientX;
if (Math.abs(diff) < 50) return; // 50px 未満は誤作動防止
this.#goTo(this.#current + (diff > 0 ? 1 : -1));
this.#restart();
}, { passive: true });
// ページ非表示時に停止(バックグラウンドタブ対策)
document.addEventListener('visibilitychange', () => {
if (document.hidden) this.#stop();
else if (this.#opts.autoplay) this.#start();
});
}
// ── 公開 API ────────────────────────────────
next() { this.#goTo(this.#current + 1); this.#restart(); }
prev() { this.#goTo(this.#current - 1); this.#restart(); }
goTo(i) { this.#goTo(i); this.#restart(); }
play() { this.#opts.autoplay = true; this.#start(); }
pause() { this.#opts.autoplay = false; this.#stop(); }
destroy() { this.#stop(); }
}
// ── 使用例 ──────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
const ss = new Slideshow('.slideshow-nav', { interval: 4000 });
// 外部から操作する例
document.getElementById('pause-btn')?.addEventListener('click', () => ss.pause());
document.getElementById('play-btn')?.addEventListener('click', () => ss.play());
});
- 自動再生(interval オプションで間隔を指定)
- 前後ボタン(手動操作後はタイマーをリセット)
- ドットインジケーター(クリックで直接ジャンプ)
- ホバーで一時停止(マウスが離れると再開)
- キーボード操作(← → キー)
- タッチスワイプ(50px 以上のスワイプで反応)
- バックグラウンドタブで停止(visibilitychange)
- loop: false でエンドレスループを無効化可能
完成形の CSS 一式
フェード切り替えタイプの完成形 CSS です。スライドアニメーション版は前のセクションの slide-track / translateX の CSS に差し替えてください。
/* ===== コンテナ ===== */
.slideshow-nav {
width: min(800px, 100%); /* レスポンシブ:最大800px */
user-select: none;
}
.slide-wrapper {
position: relative;
aspect-ratio: 16 / 9; /* 縦横比を固定(幅が変わっても崩れない) */
overflow: hidden;
border-radius: 8px;
background: #111;
}
/* ===== スライド(フェード版) ===== */
.slide-item {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.8s ease;
}
.slide-item.active { opacity: 1; }
/* ===== 前後ボタン ===== */
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(0, 0, 0, 0.45);
color: #fff;
border: none;
padding: 12px 18px;
font-size: 1.2rem;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}
.nav-btn:hover { background: rgba(0, 0, 0, 0.7); }
.nav-btn:focus-visible { outline: 3px solid #60a5fa; outline-offset: 2px; }
.prev-btn { left: 12px; }
.next-btn { right: 12px; }
/* ===== ドットナビ ===== */
.dot-nav {
display: flex;
justify-content: center;
gap: 8px;
padding: 12px 0;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #d1d5db;
border: none;
cursor: pointer;
padding: 0;
transition: background 0.2s, transform 0.2s;
}
.dot.active { background: #2563eb; transform: scale(1.3); }
.dot:focus-visible { outline: 3px solid #60a5fa; outline-offset: 3px; }
/* ===== アニメーション無効設定を尊重 ===== */
@media (prefers-reduced-motion: reduce) {
.slide-item,
.dot,
.nav-btn { transition: none; }
}
よくある質問(FAQ)
<img> に loading="eager" を指定してすべて事前読み込みする、②最初のスライドには fetchpriority="high" を付けて優先読み込みし、残りは初期化後に JS で動的に読み込む(プリフェッチ)。ページ全体の画像が多い場合は Intersection Observer で遅延読み込みと組み合わせる方法もあります。e.preventDefault() を呼ぶと縦スクロールを止められますが、passive: true オプションを付けると preventDefault は呼べなくなります。横スワイプのみ制御したい場合は、touchmove イベントで横方向の移動量が縦より大きいときだけ e.preventDefault() を呼ぶようにします。ただし passive: false はスクロールパフォーマンスに影響するため、スライドショー領域に限定して適用してください。pause() / play() メソッドをボタンのイベントに繋げるだけです。アクセシビリティの観点では、WCAG 2.1(2.2.2 停止、一時停止、非表示)のガイドラインで「5秒以上動き続けるコンテンツには一時停止機能が必要」とされています。自動再生スライドショーには一時停止ボタンを設けることを推奨します。.slide-item を <div> にして中に <img> と <p class="caption"> を入れます。キャプションには position: absolute; bottom: 0; などで画像に重ねるスタイルを適用します。テキストのコントラスト確保のため、背景に半透明の暗い帯(background: linear-gradient(transparent, rgba(0,0,0,0.6)))を重ねると読みやすくなります。まとめ
| 機能 | 実装ポイント |
|---|---|
| 自動ループ | setInterval + % images.length でインデックスをループ |
| フェードアニメーション | position: absolute + opacity + CSS transition |
| スライドアニメーション | display: flex + translateX + CSS transition |
| 前後ボタン | クリック後は clearInterval → setInterval でタイマーリセット |
| ドットナビ | 動的生成 + active クラスの切り替え |
| キーボード操作 | keydown で ArrowLeft / ArrowRight を判定 |
| タッチスワイプ | touchstart/touchend の差分が 50px 以上で切り替え |
| バックグラウンド停止 | visibilitychange で document.hidden を検知 |
CSS アニメーションの終了タイミングを JavaScript で検知する方法は画像や文字を画面の両側からフェードインさせる方法も参考になります。より基礎的なスライドショーの構造を確認したい場合はシンプルなスライドショーの作成も参照してください。