JavaScript

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

みなと

Webサイト制作において、画像やテキストを順番に切り替えて表示する「スライダー」は、非常によく使われる機能のひとつです。バナーやギャラリー、製品紹介など、さまざまな用途でスライダーが活躍します。

この記事では、Swiper.jsやslick.jsといったスライダー専用ライブラリを使わずに、jQueryを活用してスライダーを自作(フルスクラッチ)する方法を解説します。

スワン
スワン

「専用ライブラリを使えば簡単に実現できるのに、なぜ自作でスライダーを作る必要があるの?」と思う方もいるかもしれません。その疑問にお答えするために、この記事では自作スライダーのメリットについても触れながら進めていきます。

この記事では、実際に動作するデモを通じて、自作スライダーの構築方法をわかりやすく解説します。また、HTML・CSS・JavaScriptのコードを一式公開し、自動スライドやフリック操作などの実装方法も具体的に説明します。

なお、ここでいう「スライダー」は、「スライドショー」や「カルーセル」とも呼ばれるUIコンポーネントを指します。記事を通じて、自作スライダーの基礎から応用まで学び、実務や個人プロジェクトに役立てていただけたらうれしいです。それでは、さっそく始めましょう!

スライダーとは?

スライダー」とは、Webサイトで画像やテキストなどのコンテンツを順番に切り替えて表示するUIコンポーネントのことを指します。一般的には、横方向または縦方向にスライドする動きを伴うものが多く、ユーザーに視覚的な変化を与えるためによく利用されます。

みなと
みなと

フェードイン・アウトのような切り替えもスライダーの一種です。ズームインやパララックス、3Dエフェクトなど、より複雑でユニークなアニメーションを使ったスライダーもありますね。

スライダーの用途

スライダーは、Webサイトでさまざまな用途に活用されます。例えば…

  • トップページのバナー
    複数のキャンペーンやサービスを順番に表示する。
  • 製品ギャラリー
    商品画像をスライド形式で切り替え、商品の特徴を魅力的に伝える。
  • ポートフォリオ
    写真やデザイン作品を順番に閲覧できるようにする。

これらの用途では、視覚的なインパクトを与えつつ、限られたスペースで多くの情報を表示できるスライダーが非常に効果的です。

「スライドショー」や「カルーセル」との違い

スライダーは、「スライドショー」や「カルーセル」と呼ばれることもあります。これらはほぼ同じ意味で使われることが多いですが、少しだけニュアンスが異なる場合もあります。

スライドショー自動で切り替わるプレゼンテーション的なイメージが強い。
カルーセル回転木馬のように、コンテンツがループすることを強調する表現。

この記事では「スライダー」という呼び方を使用しますが、これらも同義として考えて差し支えありません。

自作スライダーのメリット

自作スライダーには、専用ライブラリでは得られないさまざまなメリットがあります。ここでは、特に注目すべき3つのポイントをご紹介します。

1. 柔軟なカスタマイズが可能

Swiper.jsslick.jsなどのスライダー専用ライブラリを使うのは一般的ですが、Web制作の現場では「仕様がまったく同じスライダー」は意外と少ないものです。プロジェクトごとの要件に応じた対応が必要になる場合が多くあります。

例えば、以下のようなケースが挙げられます。

スライダーに求められる多様な要件
  • 特定のスライドに動画を埋め込む(自動再生や停止条件を設定)。
  • スライド間の切り替え速度をスライドごとに個別設定する。
  • スライドの順番をランダム化する。
  • ページ内の他の要素をスライド切り替えに連動させる(例:背景色やタイトルの切り替え)。
  • デザイン要件に応じて特殊なレイアウトやアニメーション(ズームインや3Dエフェクトなど)を実装する。
  • スライダーの画像やテキストをCMSで簡単に更新できるようにする。

こうした要件に対応するには、スライダー専用ライブラリではカスタマイズが難しい場合もあります。そんなとき、自作スライダーならプロジェクトごとの要件に合わせて柔軟に対応できるのが最大の強みです。

みなと
みなと

私自身、ライブラリを使っていた頃は、クライアントから「ここをこう変えたい」とフィードバックをいただいたとき、「このライブラリの機能で対応できるだろうか…?」と、不安を感じることがよくありました。ライブラリに頼ると、こうした修正依頼や追加要件に柔軟に対応できるか、常に悩むことになりがちです。

2. コードとリソースを最小限に保てる

スライダーに多様な要件が求められることはよくありますが、逆に、すべての機能が必要になるわけではありません。むしろ、プロジェクトごとに不要な機能を省略することで、効率的な開発が求められるケースも少なくありません。

例えば、以下のような状況が考えられます。

  • PCのみを対象とするのでモバイル対応は不要
  • スライドは固定で4枚だけ
  • フェード切り替えだけで十分
  • 自動切り替えは不要

こうした場合、自作スライダーなら、必要な機能だけを実装することで、コードやリソースを最小限に抑えられます。その結果、サイトのパフォーマンスが向上し、ファイルの管理もシンプルになります。

スワン
スワン

専用ライブラリは、あらゆる用途に対応できる汎用性を持つ反面、不要なコードや画像、スタイルシートなどのリソースが含まれている場合があります。これらはプロジェクトによっては使われないことが多く、ファイル全体の肥大化につながることもあります。

3. 再利用性が高く効率的

自作スライダーの大きなポイントは、『作りっぱなし』で終わらないところです。一度完成させたスライダーは、テンプレートとして他のプロジェクトでも活用することができます。その結果、一から仕組みを考え直す必要がなくなり、効率的に作業を進めることが可能です。

さらに、自作スライダーを通じて「次のプロジェクトで改良を加える→さらに品質が向上する→新たなプロジェクトで活かせる」という良い循環が生まれます。一度の努力が、次回以降の負担軽減につながり、自分のスキルセットを充実させるチャンスにもなります。

スワン
スワン

一度作った自作スライダーを活かして、次のプロジェクトではちょっと手を加えるだけで新しい要件に対応できる。この効率の良さが、自作の魅力です!

自作スライダーには、柔軟なカスタマイズ性やコードとリソースの軽量化、再利用性といった多くのメリットがあります。特に、プロジェクトごとの要件に合わせた対応が求められる現場では、自作の選択肢を持っておくことが大きな強みとなります。

ただし、必ずしもすべてのプロジェクトで自作が最適というわけではありません。以下のような場合、専用ライブラリを選択するのも賢明な判断です。

  • 時間的な制約があるとき
  • シンプルなスライダーを短時間で実装したいとき

大切なのは、自作かライブラリかという手段にこだわらず、最終的な成果物の質を高めることです。 自分のスキルやプロジェクトの要件に合わせて柔軟に判断しましょう。

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

まずは、今回解説する自作スライダーがどのように動作するのか、以下のデモで確認してみてください。画像やテキストがスムーズに切り替わるシンプルなスライダーを実際に体感できます。

木製の歩道が静かな自然の中に広がっている風景

自然豊かな環境の中に広がる木製の歩道。
緑の中を静かに進むその道は、心を落ち着かせる穏やかな雰囲気を漂わせています。

ラウンジチェアとヤシの木が並ぶ芝生の風景

ヤシの木が並ぶ芝生のエリアには、ラウンジチェアが設置され、
リラックスできる空間が広がっています。

雪に覆われた斜面にあるスキーリフトの風景

雪に覆われた斜面をゆっくりと上るスキーリフト。
白銀の世界に溶け込むその風景は、ウィンタースポーツの楽しさを感じさせてくれます。

夕日を浴びる山を背景にしたプールの静かな風景

夕日が山を赤く染める中、静けさに包まれたプールが広がる風景。
自然と一体となったこの空間は、日常を忘れさせる贅沢なひとときを提供します。

機能一覧

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

自動スライド一定時間ごとに次のスライドに自動切り替え。
スライドの左右切り替え前後のスライドへ移動できる操作ボタンを搭載。
フリック&ドラッグ操作スマートフォンでのフリック操作に加え、PCでのドラッグ操作も可能。
インジケーターとの連動インジケーターボタンをクリックすることで、特定のスライドに直接移動可能。
ループ機能最後のスライドから自然に最初のスライドに戻る無限ループを実現。
ビューポート感知スライド画面内にスライダーが表示されたタイミングで自動スライドを開始。
レスポンシブ対応画面サイズに応じて最適な表示を自動調整。

デモを確認したら、次に進んでコードを見ながら仕組みを理解していきましょう。デモのコード一式はコピー&ペーストですぐに試せるようになっています。

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

以下は、今回解説するスライダーのコード一式です。HTML、CSS、JavaScriptのコードをそれぞれ記載しています。このコードを組み合わせることで、動的でレスポンシブなスライダーを簡単に試していただけます。

また、コードにはカスタマイズをしやすくするために、各箇所に詳細なコメントを多く記載しています。ぜひご自身の用途に合わせて調整してみてください!

スワン
スワン

「なんだかコードが長くて複雑そう…」と思った方もご安心ください。このあと、コードをポイントごとに詳しく解説するので、気になる部分から少しずつ理解を深めていただければと思います!

HTMLコード

スライダーの基本構造を定義した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-indicators"></div>
</div>

CSSコード

スライダーのスタイルを定義したCSSコードです。デザインやレスポンシブ対応が含まれています。

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

/* ======================== */
/* スライダー全体のスタイル */
/* ======================== */
.slider {
  position: relative;
  overflow: hidden;
  visibility: hidden;
  cursor: grab;
  -webkit-user-select: none;
          user-select: none;
}

/* スライダーの表示領域(インタラクション無効化) */
.slider-viewport {
  pointer-events: none;
}

/* スライダーのトラック(スライドを横並びに配置) */
.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;
  -webkit-mask: url(../images/slider_arrow.svg) 0 0/contain no-repeat;
          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-indicators {
  display: flex;
  justify-content: center;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}
@media print, (min-width: 768px) {
  .slider-indicators {
    margin-top: calc(56.21622% - 30px);
  }
}
@media screen and (max-width: 767px) {
  .slider-indicators {
    margin-top: calc(56.21622% - 25px);
  }
}

/* インジケーターのボタンスタイル */
.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.is-grabbing,
.slider.is-grabbing * {
  cursor: grabbing !important;
}

JavaScriptコード

スライダーの動作を実現するためのJavaScriptコードです。フリック操作や自動スライドなど、動的な処理が含まれています。

(($) => {

  const slider = (() => {
    /**
     * スライダーのプロパティを格納するオブジェクト
     */
    const props = {
      autoSlideInterval: 4000, // 自動スライドの間隔(ミリ秒単位)
      autoSlideTimer: 0, // 自動スライド用のタイマーID(clearTimeout用)
      current: 0, // 現在表示されているスライドのインデックス
      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;

      // 各スライドの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('mousedown touchstart', (e) => {
        if (!props.isAnimating) {
          handleFlick(e); // フリック操作の処理を開始
        }
      });
    }

    /**
     * ビューポートにスライダーが入ったときに自動スライドを開始するための監視を設定
     */
    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;

        // 自動スライドタイマーを再開
        startTimer();
      };
    }

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

      // イベントタイプに応じた名前を設定
      const moveEvent = (e.type === 'touchstart') ? 'touchmove.flick' : 'mousemove.flick';
      const endEvent = (e.type === 'touchstart') ? 'touchend.flick' : 'mouseup.flick mouseleave.flick';
      const origStart = (e.type === 'touchstart') ? e.originalEvent.changedTouches[0] : e.originalEvent;

      // フリックの初期位置を記録
      const touchPosition = {
        preX: origStart.pageX,
        preY: origStart.pageY
      };

      $('.slider').addClass('is-grabbing'); // グラブカーソルを表示

      // フリック中の動作を処理
      $('.slider').on(moveEvent, (e) => {
        if (props.isFlickTouch) {
          const origMove = (e.type === 'touchmove') ? e.originalEvent.changedTouches[0] : e.originalEvent;
          touchPosition.curX = origMove.pageX;
          touchPosition.curY = origMove.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) || e.type === 'mousemove') {
            props.isFlickSlide = true; // フリック操作をスライドとして認識
            e.preventDefault(); // デフォルト動作をキャンセル

            // スライドをドラッグする動きを再現
            $('.slider-items').css({
              transform: 'translateX(' + touchPosition.diffX + 'px)'
            });
          } else {
            // 垂直方向の動きや不正な操作は無効化
            props.isFlickSlide = false;
            props.isFlickTouch = false;
            $('.slider').off('.flick').removeClass('is-grabbing'); // イベントを解除
            startTimer(); // 自動スライドを再開
          }
        }
      });
      
      // フリック終了時の処理を登録
      $('.slider').on(endEvent, (e) => {
        if (props.isFlickTouch) {
          props.isFlickTouch = false; // フリック操作終了をフラグに設定
          $('.slider').off('.flick').removeClass('is-grabbing'); // イベントを解除

          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 {
            // フリックとして認識されない場合はタイマーを再開
            startTimer();
          }
        }
      });
    }

    /**
     * 自動スライドのタイマーを開始する関数
     */
    function startTimer() {
      // 既存のタイマーがあればクリア
      clearTimeout(props.autoSlideTimer);
  
      // タイマーを設定して一定時間後にスライドを移動
      props.autoSlideTimer = setTimeout(() => {
        if (!props.isAnimating) {
          moveSlide(1, 0);  // 次のスライドに移動
        }
      }, props.autoSlideInterval); // 指定された間隔で実行
    }
    
    return {
      init: init
    };
  })();
  
  // スライダーの初期化を実行
  if ($('.slider')[0]) slider.init();

})(jQuery);

コードのポイント解説

ここでは、今回のスライダーを構築する上での重要な仕組みを6つのポイントに分けて解説していきます。今回のスライダーは、動的な処理を制御するJavaScriptがその核となっています。そのため、解説もJavaScriptのコードを中心に進めていきます。

ポイント1:スライダーを初期化する仕組み

スライダーの初期化処理は、init 関数内にまとめられており、スライダーの基本構造を整えると同時に、スムーズな動作を実現する準備を行います。今回のデモでは、このinit関数で主に以下の4つの処理を実行しています。

1. クローンスライドを生成して配置

スライダーのループ機能を実現するため、先頭と末尾にクローンスライドを追加します。これにより、端のスライドからスムーズに次のスライドへ移動できる仕組みが作られます。以下のコードでは、元のスライド群の最後と最初のHTMLを取得し、それぞれ先頭と末尾に追加しています。

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

これにより、スライダー内のスライドは以下の順序で配置されます。

  1. 最後のスライド(クローンスライド)
  2. 元のスライド群
  3. 最初のスライド(クローンスライド)
スライダーのループ機能を実現するために先頭と末尾にクローンスライドを追加した配置図。スライドが [4] [1] [2] [3] [4] [1] の順で並んでいる。
スライダーのループ機能を実現するためのクローンスライドの配置例。この構成により、最初と最後のスライド間をスムーズに移動できます。

2. インジケーターの初期設定

現在表示中のスライドを視覚的に示すインジケーターを初期化します。スライド数に応じてインジケーターボタンを生成し、最初のスライドに対応するボタンを「アクティブ」状態に設定します。

// インジケーター(スライド移動ボタン)の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');

このコードでは、<button>要素をスライドの数だけ生成し、それぞれにaria-label属性を追加してアクセシビリティにも配慮しています。生成したボタンは親要素に挿入され、最初のボタンにはis-currentクラスを付与して、現在のスライド位置を示します。

3. 初期位置の設定

スライダーの初期状態では、元のスライド群の先頭が表示されるように設定します。また、先頭のクローンスライド(元のスライド群の最後の要素のコピー)は、左側に隠された状態で配置されます。この配置により、スライダーのループ動作が自然に行える仕組みを作っています。

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

このコードでは、次の処理を行っています。

  1. トラック全体の幅を設定
    クローンスライドを含むスライドの総数を基に、スライダーのトラック全体の横幅を計算します。この横幅をslider-track-wrapperに適用し、スライドが正しく並ぶようにします。
  2. トラックの初期位置を設定
    slider-track-offsetのCSSプロパティtransformを使い、トラック全体を適切な位置にシフトします。この初期位置により、元のスライド群の先頭のスライドが画面に表示され、先頭のクローンスライドが左側に隠れる状態となります。

この設定により、ユーザーが左の矢印ボタンをクリックしたり、左にフリックした際に、先頭のクローンスライドがスムーズに現れ、自然なループ動作が実現します。

スライダーの初期状態を示す図。元のスライド群の先頭が表示され、左側には先頭のクローンスライドが隠れている状態
スライダーの初期位置設定。元のスライド群の先頭が表示され、クローンスライドは左側に隠されています。

4. 初期表示の準備

スライダーを初期表示する際、見栄えを整えるために、CSSで初期状態を非表示(visibility: hidden)に設定しています。これにより、JavaScriptの初期化処理が完了するまでスライダーが表示されず、不完全な状態で描画されるのを防ぎます。

初期化が完了した段階で、以下のコードを実行し、スライダーを表示状態に切り替えます。

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

この方法により、スライダーの描画がスムーズになり、チラつきを防止できます。

ポイント2:イベントの登録方法

スライダーの操作性を実現するために、ユーザー操作に応じたイベントを登録します。この処理は、setupEvents 関数内にまとめられており、クリックやフリック操作に対応する仕組みを実現します。

1. 矢印ボタンのクリック操作

矢印ボタンでスライダーを操作できるように、クリックイベントを登録します。コードは以下の通りです。

// 矢印ボタンのクリックイベントを登録
$('.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; // クリックイベントの伝播を停止
});

このコードでは、以下の処理を行っています。

  1. アニメーション中の重複操作を防ぐ
    if (!props.isAnimating) によって、現在アニメーションが進行中かどうかを判定します。アニメーションが完了する前に再度操作されることを防ぎ、スムーズなスライド動作を保つためのチェックです。
  2. ボタンのインデックスを取得して移動方向を指定
    $('.slider-nav > button').index($(e.currentTarget)) を使用して、クリックされたボタンが左ボタンか右ボタンかを判定します。左のボタンがクリックされた場合は -1、右のボタンがクリックされた場合は 1 を移動方向として設定します。この移動方向は direction に格納され、スライドの動きを制御します。
  3. スライドを移動
    moveSlide 関数を呼び出し、指定された方向にスライドを移動させます。このとき、第2引数(dragOffsetX)はフリック操作時のドラッグ距離を表します。矢印ボタンではドラッグ操作が発生しないため、常に 0 を渡します。

2. インジケーターのクリック操作

スライダーのインジケーターは、現在表示中のスライドを視覚的に示すだけでなく、ユーザーが特定のスライドに直接移動するためにも利用できます。この機能を実現するため、各インジケーターボタンにクリックイベントを登録しています。

// インジケーターのクリックイベントを登録
$('.slider-indicators > button').on('click', (e) => {
  if (!props.isAnimating) {
    const idx = $('.slider-indicators > button').index($(e.currentTarget));
    moveSlide(idx - props.current, 0); // インジケーターのインデックスに対応するスライドに移動
  }
  return false; // クリックイベントの伝播を停止
});

このコードでは、以下の処理を行っています。

  1. アニメーション中の重複操作を防ぐ
    if (!props.isAnimating) で、スライドが移動中の場合には操作を無効化しています。このチェックは「1. 矢印ボタンのクリック操作」と同じ意図で、誤操作や重複処理を防ぐためのものです。
  2. ボタンのインデックスを取得して移動方向を指定
    $('.slider-indicators > button').index($(e.currentTarget)) でクリックされたボタンのインデックスを取得し、現在のスライド位置(props.current)との差分を計算しています。この差分が移動方向と距離を表します。計算結果を次のステップでmoveSlide関数に渡します。
    例えば、現在のスライド位置が2番目(props.current = 1)で、4番目のボタンがクリックされた場合、idx = 3 となります。このとき、差分は 3 - 1 = 2 となり、スライドを右→左に2つ移動する処理が実行されます。
  3. スライドを移動
    moveSlide(idx - props.current, 0) を呼び出して、スライドを指定の位置まで移動させます。このとき、第2引数はフリック操作が行われていないため0が渡されます。この仕組みも「1. 矢印ボタンのクリック操作」と同様です。
みなと
みなと

矢印ボタンやインジケーターのクリック範囲は、視覚的なデザイン以上に広めに設定すると操作性が向上します。特にスマートフォンで操作がしやすくなりますよ!

3. フリック操作の開始

スマートフォンやタブレットなどのデバイスでは、スライダーを指で左右にスワイプ(フリック)する操作が一般的です。この機能を実現するために、フリック操作の開始時に特定のイベントを登録し、後続の処理へつなげています。以下のコードが該当部分です。

// フリック操作の開始イベントを登録
$('.slider').on('mousedown touchstart', (e) => {
  if (!props.isAnimating) {
    handleFlick(e); // フリック操作の処理を開始
  }
});

このコードでは、以下の処理を行っています。

  1. アニメーション中の操作を防ぐ
    if (!props.isAnimating) の条件により、スライドアニメーションが実行中の場合は処理を停止します。これにより、アニメーションが完了する前に新たな操作が行われることを防ぎます。
    ここでも「1. 矢印ボタンのクリック操作」や「2. インジケーターのクリック操作」と同じく、重複操作を防ぐ仕組みです。
  2. フリックイベントの開始
    フリック操作を処理する関数 handleFlick を呼び出します。この関数内で、スライドの移動距離や方向を計算する準備が行われ、操作中の動作がスムーズになるよう設定されています。

このコードでは、マウスイベント(mousedown)とタッチイベント(touchstart)の両方を処理します。これにより、スマートフォンやタブレットなどのタッチデバイスだけでなく、PCでのドラッグ操作にも対応しています。

ポイント3:画面内に入ったときにスライドを開始する工夫

スライダーが画面内に初めて入ったタイミングで、自動スライドを開始する仕組みを導入しています。この処理は、setupIntersectionObserver 関数内にまとめられており、スライダーが画面外にある間に無駄にタイマーが進行するのを防ぎつつ、ユーザーがスライダーに到達したときに適切なタイミングで動作を開始します。コードは以下の通りです。

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]);
}

このコードでは、以下の処理を行っています。

1. Intersection Observer の設定

Intersection Observer は、指定した要素がビューポート(ブラウザの画面に表示されている部分)内に入ったかどうかを検知するためのAPIです。主な設定内容は以下の通りです。

  • root: null
    監視の基準をビューポート(画面全体)に設定します。これにより、監視対象が画面の内外に移動したかどうかを判定できます。
    特定の要素を基準にしたい場合は、その要素を root に指定することで、ビューポートの代わりにその要素のスクロール範囲を基準として監視することも可能です。
  • rootMargin: '0px'
    監視対象の要素がビューポート内に入るタイミングを微調整できます。今回の設定ではビューポートの境界そのものが基準となり、要素が境界に触れるタイミングでトリガーが発動します。
    例えば rootMargin: '-50px' とすると、ビューポートの内側50pxに達した時点でトリガーが発動します。
  • threshold: 0
    要素がビューポート内にどの程度見えるときにトリガーを発動させるかを指定します。0 は「要素が1ピクセルでもビューポート内に入ったら発動」という設定です。
    例えば threshold: 1.0 を指定すると、要素が完全にビューポート内に収まったタイミングで発動します。さらに、threshold: 0.5 を指定すると、要素がビューポート内に半分見えたタイミングでトリガーが発動します。
スライダーがビューポート外からビューポート内に一部だけ入る様子
Intersection Observer の設定内容(root: null、rootMargin: ‘0px’、threshold: 0)に基づき、スライダーがビューポート内に少しでも入ったタイミングでトリガーが発動します。

2. コールバック関数の設定

監視対象がビューポート内に入るたびにコールバック関数が呼び出されます。

  • entry.isIntersecting
    監視対象がビューポート内に入ったかどうかを判定します。true の場合はビューポート内に一部が見えており、false は完全にビューポート外にある状態です。
  • props.isStart
    スライダーがすでに動作を開始しているかを管理するフラグで、初期状態では false に設定されています。監視対象が初めてビューポート内に入った際にのみ startTimer を実行し、同時に props.isStarttrue に更新することで、タイマーの重複起動を防ぎます。
スワン
スワン

つまり、setupIntersectionObserver は「スライダーが初めて画面内に入るときに一度だけ実行する処理」を担う関数と言えます。

3. 監視対象の設定

observer.observe($('.slider')[0]); で、スライダー全体を監視対象に設定しています。これにより、スライダーが画面内に入ったタイミングで処理が実行されます。

ポイント4:スライドを動かすロジック

スライダーの動作ロジックは、moveSlide 関数内にまとめられており、ユーザーの操作や自動スライドによってスライドを動かすための中心的な役割を果たします。この関数では、スライドの方向や次の位置を計算し、アニメーションでスライドを移動させた後、状態を適切に更新する一連の処理が行われます。今回のデモでは、この moveSlide 関数で主に以下の3つの処理を実行しています。

1. スライドの方向と次のスライドの計算

スライダーを動かす際には、現在のスライド位置(props.current)と移動方向(direction)をもとに、次に表示するスライドの位置を計算します。次のスライドのインデックスを計算するコードは以下のようになっています。

// 次に表示するスライドのインデックスを計算
let next = props.current + direction;
if (next >= props.length) next -= props.length; // 最後のスライドから最初に戻る
if (next < 0) next += props.length; // 最初のスライドから最後に戻る

direction は、スライドが「右→左」に移動する場合は正の値、「左→右」に移動する場合は負の値が渡されます。また、フリック操作中に十分な距離をドラッグせず、スライドの切り替えが発生しなかった場合は、direction0 になります。この場合、現在のスライド位置に留まる処理が実行されます。

2. アニメーションの設定

次に、スライドの移動を視覚的に表現するアニメーションを設定します。moveSlide 関数では、Web Animations APIanimate メソッドを使用してスライダー要素にアニメーション効果を適用します。

Web Animations APIとは?

Web Animations API は、JavaScript から直接 CSS アニメーションや SVG アニメーションを制御できるブラウザ標準の機能です。2025年現在、主要なモダンブラウザで問題なく動作します。

  • jQuery より高速
    jQuery の animate() メソッドでは transform などの制御が難しく、アニメーションが滑らかに動作しない場合がありますが、Web Animations API はスムーズかつ効率的にアニメーションを実現します。
  • CSS より柔軟
    JavaScript で CSS アニメーションを扱う場合、直感的な操作や複雑な制御が難しい場面がありますが、Web Animations API では柔軟な制御が可能です。
  • 軽量で便利
    外部ライブラリが不要で、コード量を抑えながら、複雑なアニメーションを効率よく作成できます。

以下はアニメーション部分のコードです。

// スライドの幅を取得してアニメーションを設定
const areaW = $('.slider').outerWidth();
$('.slider-items')[0].animate([
  { transform: 'translateX(' + dragOffsetX + 'px)' }, // ドラッグ位置から開始
  { transform: 'translateX(' + (-areaW * direction) + 'px)' } // 指定方向に移動
], {
  duration: props.slideDuration, // アニメーションの時間
  easing: props.slideEasing, // イージング関数
}).onfinish = () => {
  // アニメーション完了後の処理
};

このコードでは以下の処理を行っています。

  1. スライダーの幅を取得
    outerWidth() メソッドでスライダー全体の幅を取得し、スライドを移動させる距離を計算します。たとえば、スライダーの幅が 1000px で方向が右→左の場合、スライダー全体を -1000px 移動させます。
  2. アニメーションの開始位置と終了位置を指定
    ドラッグ操作のオフセット位置 dragOffsetX から開始し、スライダーを計算された距離分移動します。これにより、フリック操作時の位置に応じたスムーズなアニメーションが実現されます。
  3. アニメーションの設定
    • duration:アニメーションの所要時間をミリ秒単位で指定します。
    • easing:アニメーションのイージング関数を指定します。
  4. アニメーションの完了時の処理
    アニメーションが終了すると onfinish イベントが発火し、次の処理(スライド位置の更新など)が実行されます。

3. スライド後の処理と状態更新

スライダーがアニメーションを完了した後、次のスライド操作に備えるための状態更新処理を行います。この処理は、moveSlide 関数内のアニメーション完了時に実行されます。以下が該当部分のコードです。

.onfinish = () => {
  // トラック位置の調整と状態のリセット
  $('.slider-track').css({
    transform: 'translateX(' + (-next / (props.length + 2) * 100) + '%)'
  });
  $('.slider-items').css({
    transform: 'translateX(0px)'
  });
  
  // 状態を更新
  props.current = next;
  props.isAnimating = false;

  // 自動スライドタイマーを再開
  startTimer();
};

このコードでは以下の処理を行っています。

  1. トラック位置の調整と状態のリセット
    アニメーション完了時には、次に表示すべきスライドが正しく画面中央に表示されている状態になります。ただし、このままでは次のスライド移動ができないため、内部的な調整を行います。
    具体的には、.slider-items の位置を translateX(0) にリセットし、リセットによる影響を補正するために、.slider-track の位置を translateX(...) で調整します。この操作により、見た目には変化がない状態を保ちながら、次回のスライド移動に備えた準備が整います。
  2. 状態の更新
    • 現在のスライド位置の更新
      props.current を次に表示するスライド位置(next)に更新します。これにより、次回のスライド操作時に、現在のスライド位置を基に正しい計算が行われるようになります。
    • アニメーション状態の解除
      props.isAnimatingfalse に設定し、次のスライド操作を受け付けられる状態に戻します。
  3. 自動スライドタイマーの再開
    アニメーション終了後に startTimer() を呼び出し、自動スライドのタイマーを再開します。

ポイント5:フリック操作の動きを実現

フリック操作を実現するためには、ユーザーのタッチやマウス操作に応じてスライダーを動かすロジックを組み込む必要があります。この処理は、handleFlick 関数内にまとめられており、次のようなステップで構成されています。

1. フリック操作の開始

フリック操作を開始する際には、タッチまたはマウス操作が検知された段階で、必要な初期化処理を行い、操作準備を整えます。以下のコードでその処理を実現しています。

clearTimeout(props.autoSlideTimer); // 自動スライドのタイマーを停止
props.isFlickSlide = false; // フリック操作によるスライドの状態をリセット
props.isFlickTouch = true; // フリック操作中であることをフラグに設定

// イベントタイプに応じた名前を設定
const moveEvent = (e.type === 'touchstart') ? 'touchmove.flick' : 'mousemove.flick';
const endEvent = (e.type === 'touchstart') ? 'touchend.flick' : 'mouseup.flick mouseleave.flick';
const origStart = (e.type === 'touchstart') ? e.originalEvent.changedTouches[0] : e.originalEvent;

// フリックの初期位置を記録
const touchPosition = {
  preX: origStart.pageX,
  preY: origStart.pageY
};

$('.slider').addClass('is-grabbing'); // グラブカーソルを表示

このコードでは以下の処理を行っています。

  1. タイマーの停止とフラグのリセット
    フリック操作中は、自動スライドが不要となるため、clearTimeout(props.autoSlideTimer) でタイマーを停止します。また、props.isFlickSlidefalse に設定してスライドの状態を初期化し、props.isFlickTouchtrue に設定することで、現在フリック操作中であることを示します。
  2. イベントタイプの設定
    タッチデバイス(スマートフォンなど)とマウスデバイス(PCなど)でイベントの名称が異なるため、touchstartmousedown のどちらかに応じて適切なイベント名を設定します。これにより、さまざまなデバイスでのフリック操作に対応可能です。
  3. 初期位置の記録
    タッチまたはマウスの開始位置を取得し、touchPosition オブジェクトに記録します。この初期位置を基準として、フリック操作中の移動量を計算します。
  4. カーソルの変更で操作をわかりやすく
    is-grabbing クラスを追加して、ドラッグ中であることを示すカーソル(グラブカーソル)を表示します。

2. フリック中の動作

フリック操作が進行中(タッチまたはマウス操作中)は、現在の位置を追跡し、スライドを動かす視覚的な演出をリアルタイムで行います。この処理は以下のコードで実現されています。

// フリック中の動作を処理
$('.slider').on(moveEvent, (e) => {
  if (props.isFlickTouch) {
    const origMove = (e.type === 'touchmove') ? e.originalEvent.changedTouches[0] : e.originalEvent;
    touchPosition.curX = origMove.pageX;
    touchPosition.curY = origMove.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) || e.type === 'mousemove') {
      props.isFlickSlide = true; // フリック操作をスライドとして認識
      e.preventDefault(); // デフォルト動作をキャンセル

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

このコードでは以下の処理を行っています。

  1. フリック中の位置を計算
    フリック操作が進行するたびに、現在のタッチまたはマウス位置を取得し、初期位置(preX)との差を計算します。この差分(diffX)がスライドの移動量を表します。
  2. 水平方向の動きを判定
    垂直方向の移動(diffY)が横方向の移動(diffX)に比べて大きい場合、フリック操作を無効化します。この処理により、縦スクロールなど他の操作と競合するのを防ぎます。
    横方向のフリックが有効と判断された場合、props.isFlickSlidetrue に設定し、デフォルトのスクロール動作をキャンセルします(e.preventDefault())。
  3. リアルタイムでスライドを移動
    横方向の移動量(diffX)に応じて、.slider-items 要素を移動させます。
  4. 無効なフリック操作の終了処理
    フリック操作が無効と判断された場合、props.isFlickSlideprops.isFlickTouch をリセットします。また、.flick イベントを解除し、グラブカーソルを非表示にします。さらに、startTimer() を呼び出して自動スライドを再開します。

3. フリック操作の終了

フリック操作が終了したときの処理では、フリック操作の移動量に応じて、スライドを切り替えるか元の位置に戻すかを判定し、必要な処理を実行します。この処理は以下のコードで実現されています。

// フリック終了時の処理を登録
$('.slider').on(endEvent, (e) => {
  if (props.isFlickTouch) {
    props.isFlickTouch = false; // フリック操作終了をフラグに設定
    $('.slider').off('.flick').removeClass('is-grabbing'); // イベントを解除

    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 {
      // フリックとして認識されない場合はタイマーを再開
      startTimer();
    }
  }
});

このコードでは以下の処理を行っています。

  1. フリック操作の終了を判定
    props.isFlickTouch フラグを false に設定し、現在のフリック操作を終了します。同時に、.flick イベントを解除し、.slider 要素からグラブカーソルを非表示にします。
  2. フリック距離に応じたスライド操作
    フリック操作がスライドとして有効(props.isFlickSlide === true)である場合、以下の基準に基づいてスライドの動作を決定します。
    • 左→右にスライド
      touchPosition.diffX がスライダーの幅(areaW)の 10% を超えた場合、スライドを左から右へ移動させます。
    • 右→左にスライド
      touchPosition.diffX がスライダーの幅(areaW)の -10% を下回った場合、スライドを右から左へ移動させます。
    • 元の位置に戻る
      フリック距離が 10% に満たない場合、スライドを元の位置に戻します。フリック距離の閾値(10%)は直感的な操作感を提供しつつ、誤操作を防ぐために設定されています。
  3. タイマーの再開
    フリック操作がスライドとして認識されなかった場合、startTimer() を呼び出して自動スライドを再開します。

ポイント6:自動スライドのタイマー管理

スライダーの自動スライド機能は、startTimer 関数を中心に実装されています。この関数では、一定時間ごとにスライドを自動的に切り替える処理を管理します。コード全体は以下のようになっています。

function startTimer() {
  // 既存のタイマーがあればクリア
  clearTimeout(props.autoSlideTimer);

  // タイマーを設定して一定時間後にスライドを移動
  props.autoSlideTimer = setTimeout(() => {
    if (!props.isAnimating) {
      moveSlide(1, 0);  // 次のスライドに移動
    }
  }, props.autoSlideInterval); // 指定された間隔で実行
}

このコードでは、以下の処理を行っています。

  1. 既存のタイマーをクリア
    clearTimeout(props.autoSlideTimer) を使用して、すでに設定されているタイマーをクリアします。これにより、タイマーが重複して設定されるのを防ぎます。
  2. 新しいタイマーの設定
    setTimeout を使用して新しいタイマーを設定します。タイマーが指定した時間(props.autoSlideInterval)経過すると、moveSlide(1, 0) が呼び出され、次のスライドに移動します。
    • props.autoSlideInterval
      自動スライドの間隔(ミリ秒)を定義するプロパティです。デモでは 4000ms(4秒)が設定されています。
    • moveSlide(1, 0)
      第一引数の 1 によってスライドを右→左に移動します。第二引数の 0 はドラッグオフセットがないことを示します。

まとめ

本記事では、自作スライダーの実装について、スムーズな動作を実現するためのポイントを解説しました。専用ライブラリを使わずにスライダーを作成することで、プロジェクトの要件に応じた柔軟なカスタマイズが可能になり、不要なコードやリソースを省くことで、軽量かつ最適なスライダーを構築できます。

自作スライダーのメリットは、コードを理解しながら自由に調整できる点にあります。そして、一度自作して仕組みを理解できると、「こんな動きを加えたら面白そう」「こうすればもっと使いやすくなるかも」といったアイデアが浮かび、自分なりのカスタマイズを楽しめるようになります。Web制作の楽しさを実感しながら、ぜひオリジナルのスライダー作りにも挑戦してみてください!

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

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