簡単！FlexBoxで作るカルーセルスライダー

## カルーセルとは?
大体こういうやつです。
皆様もLPのレヴューセクションなどで一度や二度は目にしたことがあるのではないでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/fc46156f-97d0-9e52-541c-33f1d3e728bd.png)
左右のボタンを押すとレヴューがその方向にヒュッ！とスライドし、他のレヴューが現れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/8e2da359-2679-1b1c-73dc-86041d4baaee.png)

よく見ると下の白い丸が点灯し、何番目のレビューかが分かるようにもなっています。
こういうのカッコいいですよね。
現物は[こちらのCodePen](https://codepen.io/skipeverylunch/pen/GRyqxmX)からご覧になれます。

## 結局どうやってるの？
原理的には至ってシンプルです。
まず、FlexBoxを使ってレヴューを横に並べるのですが、この際
- レヴューの横幅を親要素と同じまで広げる
- 親要素に``overflow:hidden``を設定する

ことで、選択されていないレヴューを画面の外に追い出すことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/d1e4bd46-6ae5-5964-9359-cf42463a3bab.png)
表示するレヴューを変更する方法もシンプルです。
レヴュー全体に``transform:translateX(-100%)``を加えれば、全体が左に動き、最初のレヴューの右側にあったレヴューが親要素に入り、見えるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/1e9ad90a-85ff-a891-c25f-8e93cb0ec0c9.png)
``transform:translateX(100%)``に書き換えれば、逆に右に動き、一つ左のレヴューが見えるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/7a298674-248e-5f56-87b1-e4d9f07b28f4.png)
左右のボタンを押したらこの処理を実行するよう、JavaScriptを組めばいいだけです。

## HTML & CSS
それでは、今言ったことを実装していきます。
レイアウトを実現するにはいくつか方法がありますが、今回はflexboxを使用します。垂直方向のflexboxの中に、水平方向のflexboxを入れ子にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/eaf2848c-f593-d1d1-7e83-7be5bdec6cdb.png)
レヴューを画面幅まで広げるには以下のようなレイアウトを作ります。
ポイントは、
- 左ボタン、3つを入れるコンテナ、右ボタンをflexで並べる
- コンテナを``flex:1 1 auto``に設定する。これで左ボタン+コンテナ+右ボタンの長さが画面幅と同じになる
- コンテナ内にレヴュー3つをflexで並べる。
- レヴューに``flex:1 0 100%``を設定する。これでレヴューはコンテナが同じ幅まで広がり、ひとつのレヴューのみがコンテナ内に収まる
- コンテナに``overflow:hidden``を設定し、コンテナからはみ出している2つのレヴューを表示されないようにする


ことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680862/5ca61bfa-0af6-a9c5-266b-55334f29f4d8.png)


```html:index.html
<head>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.0/css/all.min.css" integrity="sha512-10/jx2EXwxxWqCLX/hHth/vu2KY3jCF70dCQB8TSgNjbCVAC/8vai53GfMDrO2Emgwccf2pJqxct9ehpzG+MTw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body>

  <div class="vertical-container">
    <div class="horizontail-container">
      <button class="btn" id="prev">
        <i class="fa-solid fa-angle-left fa-xl"></i>
      </button>
      <div class="reviews-container">
        <div class="review">
          <div class="review-image"></div>
          <p class="review-content">
            これはとても素晴らしい商品です。あまりにも素晴らしすぎてなんかもうビックリしました。
          </p>
          <h3 class="review-name">
            田中　太郎
          </h3>
        </div>
        <div class="review">
          <div class="review-image"></div>
          <p class="review-content">
            みんな「すごい」「ヤバい」って言ってるけどこれそんなに凄いか? ぶっちゃけステマじゃね?
          </p>
          <h3 class="review-name">
            鈴木　次郎
          </h3>
        </div>
        <div class="review">
          <div class="review-image"></div>
          <p class="review-content">
            This product is..absolutely awesome, far beyond my expectation. Everyone should buy this!!!
          </p>
          <h3 class="review-name">
            John Smith
          </h3>
        </div>
      </div>
      <button class="btn" id="next"> <i class="fa-solid fa-angle-right fa-xl"></i></button>
    </div>
    <div class="indicator-container">
      <div class="indicator active"></div>
      <div class="indicator"></div>
      <div class="indicator"></div>
    </div>
  </div>
</body>
```
```css:style.css
h3,
p {
  margin: 0;
}
.vertical-container {
  display: flex;
  flex-direction: column;
  margin: 15px;
}
.horizontail-container {
  display: flex;
  gap: 15px;
  align-items: center;
}
.reviews-container {
  flex: 1 1 auto;
  display: flex;
  overflow: hidden;
}
.review {
  flex: 1 0 100%;
  text-align: center;
  margin: 15px 0 25px;
  display: grid;
  place-items: center;
  gap: 15px;
  transition-duration: 0.5s;
}
.review-image {
  width: 75px;
  height: 75px;
  background-color: #778899;
  border-radius: 50%;
}
.indicator-container {
  display: flex;
  justify-content: center;
  gap: 25px;
}
.indicator {
  height: 15px;
  width: 15px;
  border-radius: 50%;
  border: solid 1px gray;
}
.btn {
  z-index: 10;
  background-color: transparent;
  border: none;
  cursor: pointer;
}

```
これだけで、相当それっぽい画面が出来たはずです。
あとはJavaScriptを組むだけです。

## JavaScript
スライドを実装するための戦略は以下の通りです。
- ボタンとレヴュー3つを``querySelector``や``querySelectorAll``で拾う
```js:slider.js
const btns = document.querySelectorAll(".btn");
const reviews = document.querySelectorAll(".review");
```

- 現在表示するレヴューを表すインデックスを``let``で実装する
```js:slider.js
let currentIdx = 0;
```

- 左右のボタンを押すと、以下二つのことが起きるようにする。
1. 押したボタンに合わせてインデックスが増減するようにする。ただしインデックスが0より小さく成ったり、レビューの数-1より大きくならないように条件分岐を設定する。
2. 3つのレヴュー全ての``transform: translateX``の値を、インデックス数 × 100%に変更する。

- また、ボタンクリックイベント中に重ねてボタンをクリックしても無効になるよう、イベント実行中であることを表す変数``isMoving``をletで実装する。

```js:slider.js
//レヴュー3つををスライドする
const moveSlider = () => {
  reviews.forEach((el) => {
    el.style.transform = `translateX(-${currentIdx * 100}%)`;
  });
};
const handleButtonClick = (e) => {
  if (!isMoving) {
    isMoving = true;
    if (e.currentTarget.id === "prev" && currentIdx > 0) {
      currentIdx--;
    } else if (
      e.currentTarget.id === "next" &&
      currentIdx < reviews.length - 1
    ) {
      currentIdx++;
    }
    moveSlider();
    isMoving = false;
  }
};
btns.forEach((btn) => {
  btn.addEventListener("click", handleButtonClick);
});
```
また、下の白い丸の点灯は以下のようにして実現します。
- ボタンが白くなるのはCSSのクラス``active``で表現する。
```css:style.css
.active {
  background-color: gray;
}
```
- 現在インデックスが差しているレヴューのみに``active``クラスを付与する（ほかのレヴューからは消去する）関数を作る。
```js:slider.js
const moveIndicator = () => {
  indicators.forEach((el) => {
    el.classList.remove("active");
  });
  indicators[currentIdx].classList.add("active");
};
```
- ボタンクリックの中で、その関数を実行する。
```js:slider.js
const handleButtonClick = (e) => {
  if (!isMoving) {
    isMoving = true;
    if (e.currentTarget.id === "prev" && currentIdx > 0) {
      currentIdx--;
    } else if (
      e.currentTarget.id === "next" &&
      currentIdx < reviews.length - 1
    ) {
      currentIdx++;
    }
    moveSlider();
    moveIndicator();
    isMoving = false;
  }
};
```
これで動くようになりました。

## 所感
作ってる時はシンプルだと感じたのですが、言葉で説明するとあまり簡単ではない気がしてきました……。

## 参考
[Build a Static Site with Figma & Astro #8 - Infinite Slider](https://www.youtube.com/watch?v=IvRf6JqAmWE)