ライブラリ不使用(VanillaJS)で自然な挙動の無限スライダーを実装する方法

## はじめに
とある業務の中で「ライブラリを使わずにスライダーを実装してほしい」という実装指示がありました。

その中で実装したコードについて、他の記事やChatGPTを参照しており自分の中での理解度が弱かった部分があったため、備忘録や自分の中での復習の意味も込めてVanilla JSでのスライダーの実装方法について解説してみようと思います。

## 実装例
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="vEYPgJr" data-pen-title="vanilla js slider" data-user="N4gi-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/N4gi-dev/pen/vEYPgJr">
  vanilla js slider</a> by n4gi-dev (<a href="https://codepen.io/N4gi-dev">@N4gi-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

このように画面全体に画像が表示され、一定間隔で左方向に流れ続けるスライダーを想定しています。
(今回の例では画像は4枚を想定しています)

ポイントとしては**4枚目から1枚目に遷移する時にも1〜3枚目と同じ挙動をしている**点です。このあたりの挙動がVanillaJSでスライダーを作成しようとする際に詰まりがちな点だと思います。

## HTML
スライダーを横並びさせる親要素と、その親要素を囲んではみ出しを防止させるための親要素を配置しています。

```html:index.html
<div class="slider">
  <div class="slider__inner">
    <div class="slider__item">
      <p>1</p>
    </div>
    <div class="slider__item">
      <p>2</p>
    </div>
    <div class="slider__item">
      <p>3</p>
    </div>
    <div class="slider__item">
      <p>4</p>
    </div>
    <div class="slider__item">
      <p>1</p>
    </div>
  </div>
</div>
```
ポイントとしては**スライドの1枚目の画像を(スライド数+1)枚目にも配置する**という部分です。

スライドの切り替わりで画像が動くまでの間隔で末尾の画像から1枚目の画像に遷移することで、ユーザー視点で同じ方向に動き続けているように見せることができます。

![explanation1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4021196/69ce9963-c5b6-483f-b8c2-edb721e39223.jpeg)

## CSS
```css:style.css
.slider{
  width: 100%;
  overflow: hidden;
}

.slider__inner{
  width: 500%; /* 今回はHTMLの画像数が(4+1)=5枚のため */
  height: 200px; /* プロジェクトごとに適宜変更してください */
  display: flex;
  transition: transform 0.5s ease;
}

.slider__item {
  width: 100%;
  height: 200px;  /* プロジェクトごとに適宜変更してください */
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}
/* .slider__itemの各要素に背景色をつけるCSS、内側の文字のCSSは省略 */
```
Vanilla JSのスライダーでは画像の親要素のWidthを`(HTML上の画像数 * 100)%`に設定します。そうすることでスライドできるだけの領域を確保することができます。

そのままだと画面幅がはみ出てしまうため必ずもう1階層上の要素に`width: 100%;`と`overflow: hidden;`を設定してはみ出しを防止するようにするとよいです。


## JavaScript
```javascript:main.js
document.addEventListener("DOMContentLoaded", () => {
    const sliderInner = document.querySelector(".slider__inner");
    const slides = Array.from(sliderInner.children);
    let currentIndex = 0;

    const switchSlide = () => {
        currentIndex = (currentIndex + 1) % slides.length;

        if(currentIndex === 4){
            sliderInner.style.transform = `translateX(-${currentIndex * (100 / (slides.length))}%)`;
                setTimeout(() => {
                sliderInner.style.transition =  "none";
                sliderInner.style.transform =  "translateX(0%)";

                setTimeout(() => {
                    sliderInner.style.transition = 'transform 0.5s ease'; 
                    switchSlide();
                }, 500);
            }, 500)
        }else{
            sliderInner.style.transform = `translateX(-${currentIndex * (100 / (slides.length))}%)`;
        }
        
    }
    setInterval(() => switchSlide(), 5000);
});
```

ここから個別のコード解説です。

```javascript
currentIndex = (currentIndex + 1) % slides.length;
```

スライダーの順序捕捉変数`currentIndex`は、スライダーを変化させる処理の際に**剰余付きで加算**させます。

こうすることでスライダーの処理を繰り返してもindex番号がスライダーの要素数以内に収まるため、エンドレスに処理を続けることができます。

```javascript
 sliderInner.style.transform = 
    `translateX(-${currentIndex * (100 / (slides.length))}%)`;
```

基本的なスライダーの処理です。

スライダーの原理として**親要素の`transformX`を徐々にマイナス方向にずらすことによって画像の動きを実現**させています。
(100÷子要素の数)だけ1回スライドするごとに親要素の`transform: transformX`の値を変化させており、この動きにより画像を左方向に移動するような動きが実現できます。


```javascript
const switchSlide = () => {
    // ここにスライダーの処理が入ります
}
setInterval(() => switchSlide(), 5000);
```

スライダー関数は`setInterval`を使うことで一定間隔で実行することができます。これによってスライダーが動く間隔を調整できます。



```javascript
if(currentIndex === 4){
    sliderInner.style.transform = `translateX(-${currentIndex * (100 / (slides.length))}%)`;
    setTimeout(() => {
        sliderInner.style.transition =  "none";
        sliderInner.style.transform =  "translateX(0%)";
        setTimeout(() => {
            sliderInner.style.transition = 'transform 0.5s ease'; 
            switchSlide();
        }, 500); // transitionで設定した時間と合わせる
    }, 500) // transitionで設定した時間と合わせる
}
```

今回の実装ではHTMLで1枚目と最後の画像を重複させています。そのため、最後の画像になったときは**画像を遷移させたあとにトランジションなしで1枚目の画像に戻す**という処理を加えます。

今まで通りにスライド処理を行ったあと、**`transition`の時間分だけ経過したら`transition`の設定を切りつつ親要素の`transform`を初期位置に戻す処理**を行います。
こうすることでtransitionなしで要素を移動できるので、**スライダーの位置を一番最初の位置に戻す動作を閲覧者から気づかれずに行う**ことができます。

ですが、現状では次回以降のスライドの動作も`transition`設定が切れたままです。
画像の位置を変更できたら`transition`の設定を元通りに戻し、再帰的にスライダー関数を実行することで設定を戻してスライダーを動かすことができます。


## 最後に
ここまで読んでいただきありがとうございます。
今回の記事を通して自分の中でもスライダーの実装のロジックを再認識することができました。

スライダーの実装にはライブラリが便利なのは紛れもない事実ですが、プロジェクトの制約などの関係でライブラリが使えない(or不使用推奨の)ケースは存在すると思います。

そのような場面でこの記事が参考になれば幸いです。


## 参考文献
https://qiita.com/wintyo/items/a37a197f69aa205297a5

https://zenn.dev/musashi26629/articles/27557531dbcdea