【JavaScript】スライドショーの作り方完全ガイド|フェード・スライド・ドットナビ・キーボード・タッチスワイプ・Slideshowクラスまで解説

スライドショーは「画像を自動的に切り替えながらループ再生する」シンプルなものから、前後ボタン・ドットナビ・タッチスワイプ・キーボード操作まで備えた本格的なものまで幅があります。ライブラリを使えば手軽ですが、仕組みを理解して自前実装できると挙動のカスタマイズや軽量化に直結します。

この記事では画像の src を切り替える最小構成から始め、CSS トランジションによるフェード・スライドアニメーション、ナビゲーションボタン、ドットインジケーター、スマホのタッチスワイプ対応まで、段階的に機能を追加して実装します。

スポンサーリンク

最小構成:setInterval で自動ループ

最もシンプルな実装は setInterval() で一定間隔に img.src を切り替えるだけです。

HTML(最小構成)
<div class="slideshow">
  <img id="slide-img" src="image1.jpg" alt="スライド画像" width="800" height="450">
</div>
JavaScript(最小構成)
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秒ごとに切り替え
(current + 1) % images.length でループする仕組み:
%(剰余演算子)を使うと、インデックスが最後の要素を超えたとき自動的に 0 に戻ります。例: 画像が3枚なら 0→1→2→0→1→2 と繰り返します。if (current >= images.length) current = 0; と同じ意味ですが、1行で書けます。

CSS トランジションでフェードアニメーションを付ける

画像が瞬間的に切り替わると視覚的に荒く見えます。CSS の opacity トランジションを使ってスムーズなフェードを実現します。

HTML(フェード版)
<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>
CSS(フェード版)
.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;  /* アニメーション無効設定を尊重 */
  }
}
JavaScript(フェード版)
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 で各スライドの位置を制御します。

HTML(スライド版)
<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>
CSS(スライド版)
.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;
}
JavaScript(スライド版)
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);

前後ナビゲーションボタンを追加する

ユーザーが手動で前後に切り替えられる「前へ」「次へ」ボタンを追加します。

HTML(ボタン付き)
<div class="slideshow-nav" role="region" aria-label="画像スライドショー">
  <div class="slide-wrapper">
    <!-- 前後ボタン -->
    <button class="nav-btn prev-btn" aria-label="前のスライド">&#10094;</button>
    <button class="nav-btn next-btn" aria-label="次のスライド">&#10095;</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>
CSS(ボタン・ドット)
.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 クラス

自動再生・前後ボタン・ドットナビ・ホバー一時停止・キーボード操作・タッチスワイプをすべて備えた完成形クラスです。

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());
});
Slideshow クラスの機能まとめ:

  • 自動再生(interval オプションで間隔を指定)
  • 前後ボタン(手動操作後はタイマーをリセット)
  • ドットインジケーター(クリックで直接ジャンプ)
  • ホバーで一時停止(マウスが離れると再開)
  • キーボード操作(← → キー)
  • タッチスワイプ(50px 以上のスワイプで反応)
  • バックグラウンドタブで停止(visibilitychange)
  • loop: false でエンドレスループを無効化可能

完成形の CSS 一式

フェード切り替えタイプの完成形 CSS です。スライドアニメーション版は前のセクションの slide-track / translateX の CSS に差し替えてください。

完成形 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)

Qスライドが切り替わるとき画像が一瞬白くなります。原因は?
A画像の読み込みが完了する前に表示されるのが原因です。解決策は2つあります。①スライドの <img>loading="eager" を指定してすべて事前読み込みする、②最初のスライドには fetchpriority="high" を付けて優先読み込みし、残りは初期化後に JS で動的に読み込む(プリフェッチ)。ページ全体の画像が多い場合は Intersection Observer で遅延読み込みと組み合わせる方法もあります。
Qスマホで横スワイプしてもスクロールが先に動いてしまいます。
Aタッチイベントで e.preventDefault() を呼ぶと縦スクロールを止められますが、passive: true オプションを付けると preventDefault は呼べなくなります。横スワイプのみ制御したい場合は、touchmove イベントで横方向の移動量が縦より大きいときだけ e.preventDefault() を呼ぶようにします。ただし passive: false はスクロールパフォーマンスに影響するため、スライドショー領域に限定して適用してください。
Q自動再生を一時停止するボタンを付けるには?
ASlideshow クラスの pause() / play() メソッドをボタンのイベントに繋げるだけです。アクセシビリティの観点では、WCAG 2.1(2.2.2 停止、一時停止、非表示)のガイドラインで「5秒以上動き続けるコンテンツには一時停止機能が必要」とされています。自動再生スライドショーには一時停止ボタンを設けることを推奨します。
Qスライドにテキストやキャプションを重ねて表示するには?
A.slide-item<div> にして中に <img><p class="caption"> を入れます。キャプションには position: absolute; bottom: 0; などで画像に重ねるスタイルを適用します。テキストのコントラスト確保のため、背景に半透明の暗い帯(background: linear-gradient(transparent, rgba(0,0,0,0.6)))を重ねると読みやすくなります。
QSwiper.js などのライブラリとバニラ JS 実装、どちらを選ぶべきですか?
A画像が数枚で前後ボタンとドットナビだけなら、この記事のバニラJS実装で十分です。バンドルサイズを増やさずに済みます。一方で「無限ループ(最後→最初をシームレスに)」「サムネイルナビ」「タッチ慣性スクロール」「複数スライド同時表示」「縦方向スクロール」など複雑な要件がある場合は Swiper.js(約30KB gzip)の採用を検討してください。

まとめ

機能 実装ポイント
自動ループ setInterval + % images.length でインデックスをループ
フェードアニメーション position: absolute + opacity + CSS transition
スライドアニメーション display: flex + translateX + CSS transition
前後ボタン クリック後は clearIntervalsetInterval でタイマーリセット
ドットナビ 動的生成 + active クラスの切り替え
キーボード操作 keydown で ArrowLeft / ArrowRight を判定
タッチスワイプ touchstart/touchend の差分が 50px 以上で切り替え
バックグラウンド停止 visibilitychangedocument.hidden を検知

CSS アニメーションの終了タイミングを JavaScript で検知する方法は画像や文字を画面の両側からフェードインさせる方法も参考になります。より基礎的なスライドショーの構造を確認したい場合はシンプルなスライドショーの作成も参照してください。