JavaScript

自作スライダーに一時停止機能を!jQueryでUXを高める実装テクニック

みなと

これまで当ブログでは、jQueryを使ってスライダーを自作する方法を2回にわたって紹介してきました。

まずは、スライダーの基本的な仕組みを解説した入門編から ▼

あわせて読みたい
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット

続いて、サムネイル付きスライダーの実装例も紹介しました ▼

あわせて読みたい
jQueryで作る!サムネイル付きスライダーの実装テクニック【自作スライダー応用編】
jQueryで作る!サムネイル付きスライダーの実装テクニック【自作スライダー応用編】

第1回ではスライダーの基本構造と仕組みを、第2回ではサムネイル付きの応用スライダーを実装しました。今回の記事はその応用編の続きとして、「一時停止ボタン付きのスライダー」をテーマに取り上げます。

一時停止ボタンを用意することで、スライダーの動きをコントロールしやすくなり、ウェブアクセシビリティの観点からも親切な設計になります。キーボード操作やスクリーンリーダーを使うユーザーにとっても、自動再生を止められるかどうかは、情報へのアクセスしやすさに直結します。

みなと
みなと

2024年の法改正により、ウェブアクセシビリティへの対応は現場でも強く求められるようになってきました。最近では、スライダーの自動再生に対しても「ユーザーが止められるようにしてほしい」といった要望が増えており、一時停止ボタンの実装は、以前よりも現実的なニーズとして広がっています。

また今回の実装では、JavaScript(jQuery)を使って以下のような機能を実現していきます。

  • 一時停止/再生ボタンのトグル
  • スライド自動再生の制御(setTimeout/clearTimeout)
  • 経過秒数の表示(デバッグモード)

それでは早速、完成デモを見ながら、コードとそのポイントを順に解説していきましょう!

実際に動かしてみよう(デモ)

まずは、今回作成した「一時停止ボタン付きの自作スライダー」を実際に触ってみましょう。スライドは数秒ごとに自動で切り替わりますが、下部の一時停止ボタンを押すことで、任意のタイミングで再生・停止の切り替えができます。

モダンなインテリアのリビングルーム

開放感あふれるミニマルな空間に、
マスタードイエローのソファが映えるモダンリビングルーム。

緑の壁に複数のアート作品が飾られたリビングルーム

アートに囲まれた空間で彩る、
ビビッドなオレンジソファが主役のポップなリビングルーム。

白いソファとオレンジブラウンのアームチェアが置かれたリビングルーム

ナチュラルな光が差し込む、
柔らかなトーンで統一された上品なリビング空間。

キャメル色のレザーソファとグレーのアームチェアが置かれたリビング

観葉植物と木の温もりが心地よい、
ナチュラルモダンなリビング&キッチン空間。

スワン
スワン

一時停止ボタンを押すと、左上の経過時間のカウントも止まるのが確認できます。

機能一覧

このデモでは、以下の機能を実装しています。

自動スライド一定時間ごとにスライドが自動で切り替わります。
前後ナビゲーション左右の矢印ボタンで、前後のスライドに移動可能です。
インジケーター現在のスライド位置を視覚的に示し、任意のスライドにジャンプ可能です。
一時停止ボタン自動スライドを停止・再開できるボタンを設置しています。
フリック操作スマートフォンでは、指のスワイプでスライドを切り替えられます。
ループ機能最後のスライドの次に最初のスライドが表示され、無限ループします。
ビューポート感知で自動開始スライダーが画面内に表示されたタイミングで動作を開始します。
レスポンシブ対応画面幅に応じてレイアウトが最適化されます。
デバッグモード開発時に経過秒数を左上に表示して、タイマー挙動を確認できます。

コード一式(HTML・CSS・JavaScript)

ここからは、今回の「一時停止ボタン付きスライダー」の実装に使ったコードを順番にご紹介します。必要なコードはすべて掲載していますので、コピペしてそのまま動作確認が可能です。

それぞれのコードブロックにはコメントも豊富に記載していますので、ポイントとなる部分を追いやすい構成になっています。あとで「コードのポイント解説」の章で重要な部分をピックアップして解説しますが、まずはざっと全体を見てみてください。

HTMLコード

スライダー本体、各スライド要素、矢印ナビゲーション、一時停止ボタンなどを含む構造です。

<!-- スライダー全体のラッパー -->
<div class="slider">
  <!-- スライダーの表示領域 -->
  <div class="slider-viewport">
    <!-- トラック全体の幅を設定 -->
    <div class="slider-track-wrapper">
      <!-- トラック全体の初期位置を設定 -->
      <div class="slider-track-offset">
        <!-- トラック位置の調整やスライドのアニメーションを補助 -->
        <div class="slider-track">
          <!-- スライド要素を横並びに配置し、実際に移動する部分 -->
          <div class="slider-items">
            <!-- 各スライド -->
            <div class="slider-slide">
              <div class="slider-image"><img src="images/slider_photo01.webp" alt="モダンなインテリアのリビングルーム"></div>
              <p class="slider-text">開放感あふれるミニマルな空間に、<br class="only-pc">マスタードイエローのソファが映えるモダンリビングルーム。</p>
            </div>
            <div class="slider-slide">
              <div class="slider-image"><img src="images/slider_photo02.webp" alt="緑の壁に複数のアート作品が飾られたリビングルーム"></div>
              <p class="slider-text">アートに囲まれた空間で彩る、<br class="only-pc">ビビッドなオレンジソファが主役のポップなリビングルーム。</p>
            </div>
            <div class="slider-slide">
              <div class="slider-image"><img src="images/slider_photo03.webp" alt="白いソファとオレンジブラウンのアームチェアが置かれたリビングルーム"></div>
              <p class="slider-text">ナチュラルな光が差し込む、<br class="only-pc">柔らかなトーンで統一された上品なリビング空間。</p>
            </div>
            <div class="slider-slide">
              <div class="slider-image"><img src="images/slider_photo04.webp" alt="キャメル色のレザーソファとグレーのアームチェアが置かれたリビング"></div>
              <p class="slider-text">観葉植物と木の温もりが心地よい、<br class="only-pc">ナチュラルモダンなリビング&キッチン空間。</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- 矢印ナビゲーション(前後のスライドに移動) -->
  <div class="slider-nav">
    <button type="button" aria-label="前のスライドへ"></button>
    <button type="button" aria-label="次のスライドへ"></button>
  </div>
  <div class="slider-controls">
    <!-- 現在のスライド位置を示すインジケーター -->
    <div class="slider-indicators"></div>
    <!-- 自動スライドの一時停止/再開を切り替えるボタン -->
    <div class="slider-pause">
      <button type="button" aria-label="一時停止">
        <span></span>
        <span></span>
      </button>
    </div>
  </div>
</div>

CSSコード

スライダーのデザイン、アニメーション、ボタンやインジケーターのスタイルなどを定義しています。

/* ======================== */
/* レスポンシブ対応:モバイル向け調整 */
/* ======================== */
/* PC専用の改行タグを非表示にする(モバイル向けの視覚調整) */
@media screen and (max-width: 767px) {
  br.only-pc {
    display: none;
  }
}

/* ======================== */
/* スライダー全体のスタイル */
/* ======================== */
.slider {
  position: relative;
  overflow: hidden;
  visibility: hidden;
}

/* スライダーのトラック(スライドを横並びに配置) */
.slider-items {
  display: flex;
}

/* 各スライドを均等幅に設定 */
.slider-slide {
  flex: 1;
}

/* ======================== */
/* スライド内の画像のスタイル */
/* ======================== */
/* スライド内の画像を比率固定で表示 */
.slider-image {
  position: relative;
  aspect-ratio: 1480/832;
}
.slider-image img {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ======================== */
/* スライドテキストのスタイル */
/* ======================== */
.slider-text {
  line-height: 1.8;
}
@media print, (min-width: 768px) {
  .slider-text {
    margin-top: 20px;
    font-size: 16px;
    text-align: center;
  }
}
@media screen and (max-width: 767px) {
  .slider-text {
    margin-top: 15px;
    font-size: 15px;
  }
}

/* ======================== */
/* 矢印ナビゲーションのスタイル */
/* ======================== */
/* 矢印ボタンのサイズ調整 */
.slider-nav button {
  display: block;
  position: absolute;
  top: 0;
  margin: 28.10811% 0 0;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  -webkit-appearance: none;
          appearance: none;
}
@media print, (min-width: 768px) {
  .slider-nav button {
    width: 60px;
    height: 60px;
    transform: translateY(-30px);
  }
}
@media screen and (max-width: 767px) {
  .slider-nav button {
    width: 50px;
    height: 50px;
    transform: translateY(-25px);
  }
}

/* 矢印アイコンのスタイル */
.slider-nav button::before {
  content: '';
  display: block;
  position: absolute;
  top: 50%;
  background: #fff;
  mask: url(../images/slider_arrow.svg) 0 0/contain no-repeat;
  transition: background-color 300ms cubic-bezier(.215, .61, .355, 1);
}
@media print, (min-width: 768px) {
  .slider-nav button::before {
    width: 25px;
    height: 20px;
    margin-top: -10px;
  }
}
@media screen and (max-width: 767px) {
  .slider-nav button::before {
    width: 19px;
    height: 15px;
    margin-top: -7.5px;
  }
}

/* 矢印ボタンのホバー・アクティブ状態のスタイル */
@media print, (min-width: 768px) {
  .slider-nav button:hover::before,
  .slider-nav button:active::before {
    background: #c11a51;
  }
}

/* 左矢印ボタンのスタイル */
.slider-nav button:nth-child(1) {
  left: 0;
}
.slider-nav button:nth-child(1)::before {
  transform: scaleX(-1);
}
@media print, (min-width: 768px) {
  .slider-nav button:nth-child(1)::before {
    left: 15px;
  }
}
@media screen and (max-width: 767px) {
  .slider-nav button:nth-child(1)::before {
    left: 10px;
  }
}

/* 右矢印ボタンのスタイル */
.slider-nav button:nth-child(2) {
  right: 0;
}
@media print, (min-width: 768px) {
  .slider-nav button:nth-child(2)::before {
    right: 15px;
  }
}
@media screen and (max-width: 767px) {
  .slider-nav button:nth-child(2)::before {
    right: 10px;
  }
}

/* ======================== */
/* インジケーターと一時停止ボタンを横並び・中央に配置 */
/* ======================== */
.slider-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 5px;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}
@media print, (min-width: 768px) {
  .slider-controls {
    margin-top: calc(56.21622% - 32px);
  }
}
@media screen and (max-width: 767px) {
  .slider-controls {
    margin-top: calc(56.21622% - 27px);
  }
}

/* ======================== */
/* インジケーターのスタイル */
/* ======================== */
.slider-indicators {
  display: flex;
  justify-content: center;
}

/* インジケーターのボタンスタイル */
.slider-indicators button {
  display: block;
  position: relative;
  width: 22px;
  height: 22px;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  -webkit-appearance: none;
          appearance: none;
}

/* インジケーターアイコンのスタイル */
.slider-indicators button::before {
  content: '';
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: #fff;
  transition: background-color 300ms cubic-bezier(.215, .61, .355, 1), transform 300ms cubic-bezier(.215, .61, .355, 1);
}

/* インジケーターのホバー・アクティブ状態 */
@media print, (min-width: 768px) {
  .slider-indicators button:hover::before,
  .slider-indicators button:active::before {
    background: #c11a51;
  }
}

/* 現在のスライドを示すインジケーターのスタイル */
.slider-indicators button.is-current {
  pointer-events: none;
}

/* アクティブ状態のインジケーター */
.slider-indicators button.is-current::before {
  background: #c11a51;
  transform: scale(1.5);
}

/* ======================== */
/* 一時停止ボタンのスタイル */
/* ======================== */
.slider-pause button {
  display: block;
  position: relative;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 50%;
  background: none;
  appearance: none;
  cursor: pointer;
  transition: background-color 300ms cubic-bezier(.215, .61, .355, 1);
}

/* ボタンの外枠(丸型アイコンの枠線) */
.slider-pause button::before {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border: 2px solid #fff;
  border-radius: 50%;
  transition: border-color 300ms cubic-bezier(.215, .61, .355, 1);
}

/* アイコン描画用のスパン(2つ:pause/play) */
.slider-pause button > span {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

/* 一時停止(pause)アイコンの初期表示 */
.slider-pause button > span:nth-child(1) {
  display: block;
}
.slider-pause button > span:nth-child(1)::before,
.slider-pause button > span:nth-child(1)::after {
  content: '';
  display: block;
  position: absolute;
  top: 50%;
  width: 2px;
  height: 8px;
  margin-top: -4px;
  background: #fff;
}
.slider-pause button > span:nth-child(1)::before {
  left: 50%;
  margin-left: -3px;
}
.slider-pause button > span:nth-child(1)::after {
  right: 50%;
  margin-right: -3px;
}

/* 再生(play)アイコンの初期非表示 */
.slider-pause button > span:nth-child(2) {
  display: none;
}

/* play アイコン(三角形) */
.slider-pause button > span:nth-child(2)::before {
  content: '';
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 8px;
  margin: -4px 0 0 -2px;
  background: #fff;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* PC表示時:ホバー・アクティブで色を変更 */
@media print, (min-width: 768px) {
  .slider-pause button:hover,
  .slider-pause button:active {
    background: #c11a51;
  }
  .slider-pause button:hover::before,
  .slider-pause button:active::before {
    border-color: #c11a51;
  }
}

/* is-active状態で、アイコンを再生に切り替え */
.slider-pause button.is-active > span:nth-child(1) {
  display: none;
}
.slider-pause button.is-active > span:nth-child(2) {
  display: block;
}

JavaScriptコード

スライドの移動ロジック、自動再生、イベント登録、一時停止処理など、スライダー全体の挙動を制御するメイン部分です。

(($) => {

  const slider = (() => {
    /**
     * スライダーのプロパティを格納するオブジェクト
     */
    const props = {
      debugMode: true, // 【開発用】デバッグモードを有効にすると、経過秒数の表示などが可能になる

      autoSlideInterval: 4000, // 自動スライドの間隔(ミリ秒単位)
      autoSlideIntervalId: null, // 表示更新用のsetInterval ID
      autoSlideTimer: 0, // 自動スライド用のタイマーID(clearTimeout用)
      current: 0, // 現在表示されているスライドのインデックス
      elapsedSeconds: 0, // 自動スライドの経過時間(0.1秒単位)
      isAnimating: false, // スライドアニメーションが実行中かどうかのフラグ
      isFlickSlide: false, // フリック動作によるスライドが有効かどうかのフラグ
      isFlickTouch: false, // フリック動作が進行中かどうかを判定するフラグ
      isStart: false, // スライダーがビューポート内で動作を開始したかどうか
      length: 0, // スライドの総数(クローンを含まないオリジナルの数)
      slideDuration: 500, // スライドアニメーションの継続時間(ミリ秒単位)
      slideEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)' // スライドアニメーションのイージング(CSSの値形式)
    };

    /**
     * スライダーの初期化処理を行う関数
     */
    function init() {
      // スライド数を取得
      props.length = $('.slider-slide').length;

      // デバッグモード有効時に、画面右下に経過時間(秒)を表示する要素を追加
      if (props.debugMode) {
        if (props.length > 1 && $('.slider-pause').length) {
          $('.slider').append(
            '<div class="slider-timer-display" style="position: absolute; left: 0; top: 0; z-index: 1; padding: 10px 20px; background: #c11a51; color: #fff; font-size: 14px; font-weight: normal; line-height: 1.5; text-align: left;">' +
              '0.0 秒' +
            '</div>'
          );
        }
      }

      // 各スライドのHTMLを取得して配列に保存
      let htmlArray = [];
      for (let i = 0; i < props.length; i++) {
        htmlArray.push($('.slider-slide:nth-child(' + (i + 1) + ')').prop('outerHTML'));
      }

      // ループ用に先頭と末尾のクローンスライドを生成・挿入
      const firstHtml = htmlArray[props.length - 1];
      const lastHtml = htmlArray[0];
      $('.slider-items').html(firstHtml + htmlArray.join('') + lastHtml);

      // インジケーター(スライド移動ボタン)のHTMLを生成
      let indicatorsHtml = '';
      for (let i = 0; i < props.length; i++) {
        indicatorsHtml += '<button type="button" aria-label="スライド' + (i + 1) + 'に移動"></button>';
      }
      $('.slider-indicators').html(indicatorsHtml);
      $('.slider-indicators > button:first-child').addClass('is-current');

      // トラック全体の幅(クローンを含めた幅)を設定し、初期位置を調整
      $('.slider-track-wrapper').width((props.length + 2) * 100 + '%');
      $('.slider-track-offset').css({ transform: 'translateX(' + -1 / (props.length + 2) * 100 + '%)'});

      // スライダーを初期表示する
      $('.slider').css({ visibility: 'visible' });

      // イベントの登録処理を呼び出し
      setupEvents();

      // ビューポート内に入ったときにスライダーを開始
      setupIntersectionObserver();
    }

    /**
     * 各種イベントを登録する関数
     */
    function setupEvents() {
      // 矢印ボタンのクリックイベントを登録
      $('.slider-nav > button').on('click', (e) => {
        if (!props.isAnimating) {
          const idx = $('.slider-nav > button').index($(e.currentTarget));
          const direction = idx === 0 ? -1 : 1; // 左:-1, 右:1
          moveSlide(direction, 0); // 指定方向にスライドを移動
        }
        return false;
      });
    
      // インジケーターのクリックイベントを登録
      $('.slider-indicators > button').on('click', (e) => {
        if (!props.isAnimating) {
          const idx = $('.slider-indicators > button').index($(e.currentTarget));
          moveSlide(idx - props.current, 0); // インジケーターのインデックスに対応するスライドに移動
        }
        return false;
      });
    
      // フリック操作の開始イベントを登録
      $('.slider').on('touchstart', (e) => {
        if (!props.isAnimating) {
          handleFlick(e); // フリック操作の処理を開始
        }
      });
      
      // 一時停止ボタンのクリックイベントを登録
      $('.slider-pause button').on('click', (e) => {
        // 一時停止状態でなければ → 一時停止に切り替える
        if (!$('.slider-pause button').hasClass('is-active')) {
          $('.slider-pause button').addClass('is-active'); // ボタン状態を一時停止に
          clearTimeout(props.autoSlideTimer); // 自動スライド用タイマーを停止
          clearInterval(props.autoSlideIntervalId); // 経過時間表示用インターバルも停止
        } else {
          // すでに一時停止中なら → 再生を再開する
          $('.slider-pause button').removeClass('is-active'); // ボタン状態を通常に戻す
          if (!props.isAnimating) {
            startTimer(); // アニメーション中でなければ、自動スライドを再開
          }
        }
        return false;
      });
    }

    /**
     * ユーザーがスライダーを視認できる位置までスクロールしたときに初めてスライダーを開始する
     * → 無駄な処理を避け、パフォーマンスを最適化するための遅延初期化
     */
    function setupIntersectionObserver() {
      // Intersection Observerのインスタンスを作成
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          // 要素がビューポート内に入ったら、自動スライドを開始
          if (entry.isIntersecting && !props.isStart) {
            props.isStart = true; // スライダーがビューポート内に入ったことを記録
            startTimer(); // 自動スライドを開始
          }
        });
      }, {
        root: null, // ビューポートを基準に監視
        rootMargin: '0px', // マージンなし
        threshold: 0 // 要素が1ピクセルでも見えたらトリガー
      });

      // スライダーの監視を開始
      observer.observe($('.slider')[0]);
    }

    /**
     * スライダーを指定方向に移動させる
     * @param {number} direction - スライドの移動方向と距離を表す値。正の値は右→左、負の値は左→右への移動、0 はその場に留まる
     * @param {number} dragOffsetX - ドラッグ操作時のオフセット値(ピクセル単位)
     */
    function moveSlide(direction, dragOffsetX) {
      // アニメーション中はスライドの操作を無効化
      props.isAnimating = true;

      // 次に表示するスライドのインデックスを計算
      let next = props.current + direction;
      if (next >= props.length) next -= props.length; // 最後のスライドから最初に戻る
      if (next < 0) next += props.length; // 最初のスライドから最後に戻る
  
      // 現在のインジケーターを更新
      $('.slider-indicators > button.is-current').removeClass('is-current');
      $('.slider-indicators > button:nth-child(' + (next + 1) + ')').addClass('is-current');
  
      // スライドの幅を取得してアニメーションを設定
      const areaW = $('.slider').outerWidth();
      $('.slider-items')[0].animate([
        { transform: 'translateX(' + dragOffsetX + 'px)' }, // ドラッグ位置から開始
        { transform: 'translateX(' + (-areaW * direction) + 'px)' } // 指定方向に移動
      ], {
        duration: props.slideDuration, // アニメーションの時間
        easing: props.slideEasing, // イージング関数
      }).onfinish = () => {
        // トラック位置の調整と状態のリセット
        $('.slider-track').css({
          transform: 'translateX(' + (-next / (props.length + 2) * 100) + '%)'
        });
        $('.slider-items').css({
          transform: 'translateX(0px)'
        });
        
        // 状態を更新
        props.current = next;
        props.isAnimating = false;

        // 自動スライドタイマーを再開
        if ($('.slider-pause').length && !$('.slider-pause button').hasClass('is-active')) {
          startTimer();
        }
      };
    }

    /**
     * フリック操作の処理を行う関数
     * @param {Event} e - 発生したイベントオブジェクト
     */
    function handleFlick(e) {
      clearTimeout(props.autoSlideTimer); // 自動スライドのタイマーを停止
      clearInterval(props.autoSlideIntervalId); // 自動スライドの更新タイマーを停止
      props.isFlickSlide = false; // フリック操作によるスライドの状態をリセット
      props.isFlickTouch = true; // フリック操作中であることをフラグに設定

      // フリックの初期位置を記録
      const touchPosition = {
        preX: e.originalEvent.changedTouches[0].pageX,
        preY: e.originalEvent.changedTouches[0].pageY
      };

      // フリック中の動作を処理
      $('.slider').on('touchmove.flick', (e) => {
        if (props.isFlickTouch) {
          touchPosition.curX = e.originalEvent.changedTouches[0].pageX;
          touchPosition.curY = e.originalEvent.changedTouches[0].pageY;
          touchPosition.diffX = touchPosition.curX - touchPosition.preX;
          touchPosition.diffY = touchPosition.curY - touchPosition.preY;
    
          // フリック操作が水平方向かどうかを判定
          if (Math.abs(touchPosition.diffY) < Math.max(Math.abs(touchPosition.diffX), 10)) {
            props.isFlickSlide = true; // フリック操作をスライドとして認識
            e.preventDefault(); // デフォルト動作をキャンセル

            // スライドをドラッグする動きを再現
            $('.slider-items').css({
              transform: 'translateX(' + touchPosition.diffX + 'px)'
            });
          } else {
            // 垂直方向の動きや不正な操作は無効化
            props.isFlickSlide = false;
            props.isFlickTouch = false;
            $('.slider').off('.flick'); // イベントを解除

            // 自動スライドを再開
            setTimeout(() => {
              if (
                !props.isAnimating &&
                $('.slider-pause').length &&
                !$('.slider-pause button').hasClass('is-active')
              ) {
                startTimer();
              }
            }, 0);
          }
        }
      });
      
      // フリック終了時の処理を登録
      $('.slider').on('touchend.flick', (e) => {
        if (props.isFlickTouch) {
          props.isFlickTouch = false; // フリック操作終了をフラグに設定
          $('.slider').off('.flick'); // イベントを解除

          if (props.isFlickSlide) {
            const areaW = $('.slider').outerWidth(); // スライダーの幅を取得

            // フリック距離に応じてスライドを移動
            if (touchPosition.diffX > areaW / 10) {
              moveSlide(-1, touchPosition.diffX); // 左→右にスライド
              e.preventDefault();
            } else if (touchPosition.diffX < -areaW / 10) {
              moveSlide(1, touchPosition.diffX); // 右→左にスライド
              e.preventDefault();
            } else {
              moveSlide(0, touchPosition.diffX); // 元の位置に戻る
              e.preventDefault();
            }
          } else {
            // フリックとして認識されない場合はタイマーを再開
            setTimeout(() => {
              if (
                !props.isAnimating &&
                $('.slider-pause').length &&
                !$('.slider-pause button').hasClass('is-active')
              ) {
                startTimer();
              }
            }, 0);
          }
        }
      });
    }

    /**
     * 自動スライドのタイマーを開始する関数
     */
    function startTimer() {
      // 既にタイマーが存在する場合は一度クリアして二重起動を防ぐ
      clearTimeout(props.autoSlideTimer);
      clearInterval(props.autoSlideIntervalId);

      // 経過時間のカウンタをリセット
      props.elapsedSeconds = 0;

      // デバッグモード:0.1秒単位で経過時間を表示更新
      if (props.debugMode) {
        props.autoSlideIntervalId = setInterval(() => {
          props.elapsedSeconds += 0.1;
          $('.slider-timer-display').text(`${props.elapsedSeconds.toFixed(1)} 秒`);
        }, 100);
      }

      // 指定時間後に自動で次のスライドへ移動
      props.autoSlideTimer = setTimeout(() => {
        // デバッグモード時:秒数表示をリセットし、インターバルも停止
        if (props.debugMode) {
          clearInterval(props.autoSlideIntervalId);
          $('.slider-timer-display').text(`0.0 秒`);
        }

        // スライドアニメーションが実行中でないことを確認してから進行
        if (!props.isAnimating) {
          moveSlide(1, 0);  // 次のスライドに移動
        }
      }, props.autoSlideInterval); // 指定された間隔で実行
    }
    
    return {
      init: init
    };
  })();
  
  // スライダーの初期化を実行
  if ($('.slider').length) slider.init();

})(jQuery);

コードのポイント解説

今回のスライダーも、これまでの記事と同様に比較的シンプルな構成で実装していますが、一時停止機能の追加によって、いくつか新しい処理が加わっています。ここではその中でも、特に理解しておきたいポイントを2つに絞って解説していきます。

なお、スライダーの基本的な構造やイベント処理の流れ、自動スライドの考え方などについては、以下の記事で丁寧に解説しています。今回の実装と共通する部分も多いため、必要に応じてあわせてご覧ください。

あわせて読みたい
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット

ポイント1:一時停止ボタンの仕組み

今回の記事の主役ともいえる「一時停止ボタン」は、自動で切り替わるスライドの動作をユーザーが任意でコントロールできるようにする機能です。

ボタンの状態切り替えにはクラスを利用

ボタンの状態(再生中/一時停止中)は、JavaScript側で .is-active クラスを付け外しすることで切り替えています。このクラスに応じて、CSSではアイコンの見た目(Pause/Play)が変化するように記述しています。

// 一時停止ボタンのクリックイベントを登録
$('.slider-pause button').on('click', (e) => {
  // 一時停止状態でなければ → 一時停止に切り替える
  if (!$('.slider-pause button').hasClass('is-active')) {
    $('.slider-pause button').addClass('is-active'); // ボタン状態を一時停止に
    clearTimeout(props.autoSlideTimer); // 自動スライド用タイマーを停止
    clearInterval(props.autoSlideIntervalId); // 経過時間表示用インターバルも停止
  } else {
    // すでに一時停止中なら → 再生を再開する
    $('.slider-pause button').removeClass('is-active'); // ボタン状態を通常に戻す
    if (!props.isAnimating) {
      startTimer(); // アニメーション中でなければ、自動スライドを再開
    }
  }
  return false;
});

自動スライドのタイマーを停止・再開

自動スライドの制御には setTimeout() を使用し、一定時間後に次のスライドへ移動するようになっています。また、デバッグモードが有効な場合は、経過時間を視覚的に表示するために setInterval() も併用しています。

そして再開時には、startTimer() を呼び出すことでタイマーを再セットし、再び一定時間ごとにスライドが切り替わるようにしています。

ポイント2:デバッグモードで動作確認をしやすくする

今回のスライダーには、開発時に挙動を確認しやすくする「デバッグモード」が組み込まれています。この機能により、自動再生のタイミングが視覚的に把握でき、設定値の調整や挙動検証がスムーズになります。

経過秒数のリアルタイム表示

props.debugModetrue に設定されていると、スライダーの左上に 「0.0秒」のような経過時間表示用の要素が追加されます。setInterval() によって 0.1 秒単位で更新されるので、自動スライドのタイミングをリアルタイムで確認できます。

props.autoSlideIntervalId = setInterval(() => {
  props.elapsedSeconds += 0.1;
  $('.slider-timer-display').text(`${props.elapsedSeconds.toFixed(1)} 秒`);
}, 100);

自動再生と連動して停止

自動スライドの制御は setTimeout() で行い、指定時間後に次のスライドへ移動します。setInterval() はあくまで経過表示のための補助であり、実際にスライドを動かすロジックには影響しません。

また、一時停止ボタンが押された際には、経過秒数の表示用 setInterval() も同時に停止されます。これにより、ビュー表示と内部のタイマー状態が同期された形でリセットされるのが特徴です。

clearTimeout(props.autoSlideTimer);
clearInterval(props.autoSlideIntervalId);

まとめ

この記事では、一時停止ボタン付きの自作スライダーを実装する方法を紹介しました。

自動再生のスライダーに一時停止機能を加えることで、ユーザーが自分のペースでコンテンツを操作できるようになり、使いやすさが向上します。今回は、開発中の動作確認をサポートする「デバッグモード」も用意しており、今後の応用にも役立てやすい構成となっています。

この記事、そしてこれまでの2つの記事を通して、少しずつ「自分でスライダーを作る」ための考え方や実装のコツがつかめるかと思います。ぜひ、ご自身のWeb制作や学習に役立ててみてください!

自作スライダーの基本をゼロから学びたい方に▼

あわせて読みたい
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット
自作スライダー完全解説!jQueryを活用してスライダーを実装する方法とメリット

サムネイル連動などの応用例を見てステップアップしたい方に ▼

あわせて読みたい
jQueryで作る!サムネイル付きスライダーの実装テクニック【自作スライダー応用編】
jQueryで作る!サムネイル付きスライダーの実装テクニック【自作スライダー応用編】

押していただけると励みになります!

ABOUT ME
みなと
みなと
フロントエンドエンジニア
東京のWeb制作会社で15年以上働いている現役フロントエンドエンジニアです。これまで、いろんなプロジェクトに関わりながら、フロントエンド開発やWebデザインに取り組んできました。このブログでは、今までの経験を活かして、Web制作に役立つ情報やノウハウをシェアしていきたいと思います。初心者の方から、現場で働く方まで、誰でも参考にできる内容をお届けしますので、ぜひ覗いてみてください。
記事URLをコピーしました