🐧

vanillaJSで簡易的なスライダー機能を実装したい

2022/08/08に公開

書こうと思った経緯

  • 自分がカルーセル機能を素のJSで作ろうと思ったときに、ほとんどjQueryの解説記事ばかりで
    まとまったサイトが見当たらなかったから。
  • 同じようにvanillaJSでカルーセルを作りたい人が、いろいろ検索する手間を省けるようにしたいから。
  • 自分の理解の整理のため。

実装している機能

  • スライド送りボタンによる、カルーセルの動作
  • ページネーション
  • 一定時間ごとに、自動でカルーセルが動作する機能

完成図は以下
https://www.lets-retire-early.com/carousel/

この記事に記載していること

  • この記事は、https://pengi-n.co.jp/blog/carousel/ こちらの記事の手順に準じているため、
    詳細の解説は上記記事に委ねます。
  • 筆者の都合の良いように、上記記事から内容を一部改変しています。改変した部分は都度明記します。
  • 上記記事はjQueryでの解説のため、JSへの書き換えに焦点を置いて記載します。

解説

全体像の確認

実際に解説に入る前に、全体の流れを確認します。
1.HTML/CSSで大枠を作成
2.ページ送りボタンを設置
3.ページ送りボタン機能の実装
4.ページネーションの実装
5.自動スライド送り機能の実装

順番に解説していきます。

HTML/CSSで大枠を作成

画像は任意のものを使用してください。
HTMLのコードは以下です。

<div class="carousel">
  <ul class="carousel__area">
     <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-1.jpg" alt="" />
     </li>
     <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-2.jpg" alt="" />
     </li>
     <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-3.jpg" alt="" />
     </li>
     <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-4.jpg" alt="" />
     </li>
     <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-5.jpg" alt="" />
     </li>
  </ul>
</div>

SCSSのコードは以下です。フレックスボックスで画像を横並びにしています。

.carousel{
  width: 600px;
  height:calc(600px * 0.5625);
  position: relative;
  margin:0 auto;
  
  @media all and (max-width:767px){
    width:300px;
    height:calc(300px * 0.5625);
  }
  
  //カルーセルのレイアウト

  &__list{
    width: 600px;
    height:calc(600px * 0.5625);
    margin-right: 30px;
    
    @media all and (max-width:767px){
      width:300px;
      height:calc(300px * 0.5625);
      margin-right: 0;
    }
  }
  
  &__img{
    width:100%;
    height:100%;
    object-fit: cover;
  }

  &__area{
    position: absolute;
    display: flex;
    height:100%;

    @media all and (max-width:767px){
      width:1500px;
    }
  }
 }

ページ送りボタンの設置

簡易的なものなので、アクセシビリティに配慮できていません。ご理解ください。

<div class="arrowWrap">
    <div class="arrowWrap__left">
      <button class="arrowWrap__btn js__btn-back"></button>
    </div>
    <div class="arrowWrap__right">
      <button class="arrowWrap__btn js__btn-next"></button>
    </div>
 </div>

先ほどの[carousel]のdivにrelativeを指定して、[arrowWrap]を上下左右中央に設置しています。

 .arrowWrap{
  position: absolute;
  top: 0;
  left:50%;
  transform: translateX(-50%);
  width:90%;
  height:100%;
  display:flex;
  justify-content:space-between;
  align-items:center;

  &__left,&__right{
    background-color: rgba(113, 135, 245, 0.8);
    width:48px;
    height:48px;
    display:flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
  }

  &__btn{
    width:35%;
    height:70%;
    background-color: #fefefe;
    display:block;
  }

  &__left{
    .arrowWrap__btn{
      clip-path: polygon(0 50%,100% 0,100% 100%);
    }
  }
  &__right{
    .arrowWrap__btn{
      clip-path: polygon(0 0,100% 50%,0 100%);
    }
  }
}

ページ送りボタン機能の実装

1.関数の定義と実行
2.必要なDOMの取得と操作
3.スライドに番号を付与,変数化する
4.スライドの動きを関数化
5.ボタンを押したら関数実行

1.関数の定義と実行

window.addEventListener('DOMContentLoaded', function () {
  carousel();//DOMの生成後に関数を実行
});

function carousel(){
この中に記載していく
}

2.必要なDOMの取得と操作

見にくいと思うので、HTMLも併記します

<div class="carousel">
   <ul class="carousel__area">
      <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-1.jpg" alt="" />
      </li>
      <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-2.jpg" alt="" />
      </li>
      <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-3.jpg" alt="" />
      </li>
      <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-4.jpg" alt="" />
      </li>
      <li class="carousel__list">
        <img class="carousel__img" src="images/cafe-5.jpg" alt="" />
      </li>
   </ul>
   <div class="arrowWrap">
      <div class="arrowWrap__left">
        <button class="arrowWrap__btn js__btn-back"></button>
      </div>
      <div class="arrowWrap__right">
        <button class="arrowWrap__btn js__btn-next"></button>
      </div>
   </div>
</div>
  let slideLength = document.querySelectorAll('.carousel__list').length;//スライドの枚数
  const slideList = document.querySelectorAll('.carousel__list');
  let slideListStyle = getComputedStyle(slideList[0]);
  let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
  let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
  let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
  let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅
  let slideIntervalTotal = slideIntervalValue * slideLength;//
  let slideAreaWidth = slideWidth * slideLength + slideIntervalTotal + "px";//カルーセル全体の横幅
  const slideArea = document.querySelector('.carousel__area');
  slideArea.style.width = slideAreaWidth;//[.carousel__area]の横幅(width)を指定
  const slideBackBtn = document.querySelector('.js__btn-back');
  const slideNextBtn = document.querySelector('.js__btn-next');

参考サイトと違うのは、スライドの横幅に余白も足した部分です。
そうすることで、余白分だけスライドがずれていくことが無くなりました。

ポイント

outerWidth() => clientWidth
"要素の横幅の取得"

css() => style.property = '値'
"CSSの書き換え"

let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
  let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
  let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
  let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅

余白(margin-right)の値を取得。[30px]が取得される。
replaceメソッドで[30px]から[px]を除去。
上記のみでは、[30]という"文字列"が取得されるので、スライドの幅+余白の幅の計算が
文字列の結合とみなされる
そのため、Numberメソッドにて数値への変換が必要。

3.スライドに番号を付与,変数化する

let slideCurrentIndex = 0;
let slideLastIndex = slideLength - 1;

4.スライドの動きを関数化

スライドの動きを[changeSlide]関数にまとめます。

function changeSlide() {
    let slideMove = slideCurrentIndex * slideWidth;//スライドが移動する幅を指定
    slideArea.style.willChange = 'transform';//[carousel__area]にwill-change:transform;を指定
    slideArea.animate([
      { transform: `translateX(-${slideMove}px)` }
    ], {
      duration: 500,
      fill: 'forwards'
    });
   }

やっていること

5.ボタンを押したら関数実行

slideNextBtn.addEventListener('click', function () {
    if (slideCurrentIndex === slideLastIndex) {
      slideCurrentIndex = 0;
      changeSlide();
    } else {
      slideCurrentIndex++;
      changeSlide();
    }
  });
  
  slideBackBtn.addEventListener('click', function () {
    if (slideCurrentIndex === 0) {
      slideCurrentIndex = slideLastIndex;
      changeSlide();
    } else {
      slideCurrentIndex--;
      changeSlide();
    }
  })

[js__btn-next][js__btn-back]それぞれクリックイベントで[changeSlide]関数の実行。

ページネーションの実装

まずは、デザインの設定です。

<div class="carousel__pagination">
   <span class="carousel__paginationCircle js-dot target"></span>
   <span class="carousel__paginationCircle js-dot"></span>
   <span class="carousel__paginationCircle js-dot"></span>
   <span class="carousel__paginationCircle js-dot"></span>
   <span class="carousel__paginationCircle js-dot"></span>
</div>
&__pagination{
    width: 150px;
    position: absolute;
    bottom: 0;
    left:50%;
    transform: translate(-50%,200%);
    display: flex;
    justify-content:space-between;
  }

  &__paginationCircle{
    width: 20px;
    height:20px;
    border:1px solid #333;
    border-radius: 50%;
    background-color: rgba(83, 97, 223, 0.3);

    &.target{
      background-color: rgba(83, 97, 223, 0.8);
    }
  }

次に機能の実装です。以下を[changeSlide]関数の中に追記します。

 const paginationDot = document.querySelectorAll('.js-dot');
    for (let i = 0; i < paginationDot.length; i++){
      paginationDot[i].classList.remove('target');
    }//一度全ての[js-dot]から[target]クラスを除去
    paginationDot[slideCurrentIndex].classList.add('target');
    //[スライド番号]番目の[js-dot]に[target]クラスを付与

pagenationDotはNodeListです。(以下、参考サイト)
https://developer.mozilla.org/ja/docs/Web/API/NodeList
https://tech-blog.tomono.jp/archives/2486

自動スライド送り機能の実装

startTimer関数の中でTimer関数を定義します

let Timer;
  function startTimer() {
    Timer = setInterval(function () {
      if (slideCurrentIndex === slideLastIndex) {
        slideCurrentIndex = 0;
        changeSlide();
      } else {
        slideCurrentIndex++;
        changeSlide();
      };
    },3000);
  };

このままでは永遠にTimer関数が動き続けるので、これを止める関数も定義します。

function stopTimer() {
    clearInterval(Timer);
  };

ページお送りボタンのクリックイベントの中で、stopTimer関数、startTimer関数の実行を追記します

slideNextBtn.addEventListener('click', function () {
    stopTimer();
    startTimer();
    if (slideCurrentIndex === slideLastIndex) {
      slideCurrentIndex = 0;
      changeSlide();
    } else {
      slideCurrentIndex++;
      changeSlide();
    }
  });
  
  slideBackBtn.addEventListener('click', function () {
    stopTimer();
    startTimer();
    if (slideCurrentIndex === 0) {
      slideCurrentIndex = slideLastIndex;
      changeSlide();
    } else {
      slideCurrentIndex--;
      changeSlide();
    }
  });

完成したコードの確認

<div class="carousel">
  <ul class="carousel__area">
    <li class="carousel__list">
      <img class="carousel__img" src="images/cafe-1.jpg" alt="" />
    </li>
    <li class="carousel__list">
      <img class="carousel__img" src="images/cafe-2.jpg" alt="" />
    </li>
    <li class="carousel__list">
      <img class="carousel__img" src="images/cafe-3.jpg" alt="" />
    </li>
    <li class="carousel__list">
      <img class="carousel__img" src="images/cafe-4.jpg" alt="" />
    </li>
    <li class="carousel__list">
      <img class="carousel__img" src="images/cafe-5.jpg" alt="" />
    </li>
  </ul>
  <div class="arrowWrap">
    <div class="arrowWrap__left">
      <button class="arrowWrap__btn js__btn-back"></button>
    </div>
    <div class="arrowWrap__right">
      <button class="arrowWrap__btn js__btn-next"></button>
    </div>
  </div>
  <div class="carousel__pagination">
    <span class="carousel__paginationCircle js-dot target"></span>
    <span class="carousel__paginationCircle js-dot"></span>
    <span class="carousel__paginationCircle js-dot"></span>
    <span class="carousel__paginationCircle js-dot"></span>
    <span class="carousel__paginationCircle js-dot"></span>
  </div>
</div>
.carousel{
  width: 600px;
  height:calc(600px * 0.5625);
  position: relative;
  margin:0 auto;
  
  @media all and (max-width:767px){
    width:300px;
    height:calc(300px * 0.5625);
  }
  
  //カルーセルのレイアウト

  &__list{
    width: 600px;
    height:calc(600px * 0.5625);
    margin-right: 30px;
    
    @media all and (max-width:767px){
      width:300px;
      height:calc(300px * 0.5625);
      margin-right: 0;
    }
  }
  
  &__img{
    width:100%;
    height:100%;
    object-fit: cover;
  }

  &__area{
    position: absolute;
    display: flex;
    height:100%;

    @media all and (max-width:767px){
      width:1500px;
    }
  }

  //ページネーションのレイアウト

  &__pagination{
    width: 150px;
    position: absolute;
    bottom: 0;
    left:50%;
    transform: translate(-50%,200%);
    display: flex;
    justify-content:space-between;
  }

  &__paginationCircle{
    width: 20px;
    height:20px;
    border:1px solid #333;
    border-radius: 50%;
    background-color: rgba(83, 97, 223, 0.3);

    &.target{
      background-color: rgba(83, 97, 223, 0.8);
    }
  }
}

//スライド送りボタンのレイアウト

.arrowWrap{
  position: absolute;
  top: 0;
  left:50%;
  transform: translateX(-50%);
  width:90%;
  height:100%;
  display:flex;
  justify-content:space-between;
  align-items:center;

  &__left,&__right{
    background-color: rgba(113, 135, 245, 0.8);
    width:48px;
    height:48px;
    display:flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
  }

  &__btn{
    width:35%;
    height:70%;
    background-color: #fefefe;
    display:block;
  }

  &__left{
    .arrowWrap__btn{
      clip-path: polygon(0 50%,100% 0,100% 100%);
    }
  }
  &__right{
    .arrowWrap__btn{
      clip-path: polygon(0 0,100% 50%,0 100%);
    }
  }
}

window.addEventListener('DOMContentLoaded', function () {
  carousel();
})

function carousel() {
  let slideLength = document.querySelectorAll('.carousel__list').length;//スライドの枚数
  const slideList = document.querySelectorAll('.carousel__list');
  let slideListStyle = getComputedStyle(slideList[0]);
  let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
  let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
  let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
  let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅
  let slideIntervalTotal = slideIntervalValue * slideLength;//
  let slideAreaWidth = slideWidth * slideLength + slideIntervalTotal + "px";//カルーセル全体の横幅
  const slideArea = document.querySelector('.carousel__area');
  slideArea.style.width = slideAreaWidth;//[.carousel__area]の横幅(width)を指定
  const slideBackBtn = document.querySelector('.js__btn-back');
  const slideNextBtn = document.querySelector('.js__btn-next');

  let slideCurrentIndex = 0;
  let slideLastIndex = slideLength - 1;

  function changeSlide() {
    let slideMove = slideCurrentIndex * slideWidth;
    slideArea.style.willChange = 'transform';
    slideArea.animate([
      { transform: `translateX(-${slideMove}px)` }
    ], {
      duration: 500,
      fill: 'forwards'
    });

    //ページネーションの変数を定義
    const paginationDot = document.querySelectorAll('.js-dot');
    for (let i = 0; i < paginationDot.length; i++){
      paginationDot[i].classList.remove('target');
    }
    paginationDot[slideCurrentIndex].classList.add('target');
  };

  let Timer;
  function startTimer() {
    Timer = setInterval(function () {
      if (slideCurrentIndex === slideLastIndex) {
        slideCurrentIndex = 0;
        changeSlide();
      } else {
        slideCurrentIndex++;
        changeSlide();
      };
    },3000);
  };

  function stopTimer() {
    clearInterval(Timer);
  };

  startTimer();

  slideNextBtn.addEventListener('click', function () {
    stopTimer();
    startTimer();
    if (slideCurrentIndex === slideLastIndex) {
      slideCurrentIndex = 0;
      changeSlide();
    } else {
      slideCurrentIndex++;
      changeSlide();
    }
  });
  
  slideBackBtn.addEventListener('click', function () {
    stopTimer();
    startTimer();
    if (slideCurrentIndex === 0) {
      slideCurrentIndex = slideLastIndex;
      changeSlide();
    } else {
      slideCurrentIndex--;
      changeSlide();
    }
  });
}

今後実装したい機能

  • 無限ループ(最後のスライドに行ったら、ぎゅんって最初に戻らないやつ)
  • ページネーションクリックしてスライド移動できるやつ
  • ページネーションのドットを、スライド枚数で自動生成する
  • スライド表示枚数の変更
  • スライド移動の時のアニメーション
  • スワイプ対応

最後に

意味わからないところ、間違っているところはコメント頂けると嬉しいです。
vanillaJSにわかなので、コードが冗長かもしれません。
今回の経験で、基本的なカルーセルを実装できたので
今後はもっと機能を増やして行きたいと思います。

P.S

誰か、一周目に謎にぎゅんってなるバグの原因教えてください。。。

Discussion