【Swiper v11】サムネイルが centeredSlides で常に中央にならなくて詰まった話

##  はじめに
スライドショーが手軽に実装できる JavaScript ライブラリの Swiper.js ですが、カスタマイズに苦労することも多いですよね。
私自身、解決策を探す中で参考サイトが古いバージョンの情報しかなかったり、GitHub の Issue を調べても解決策が見つからなかったりと行き詰まりました。そんな中、試行錯誤を繰り返しながら「こうすればいける！」という方法に辿り着いたのでこの記事で共有したいと思います。

今回ご紹介する方法が必ずしも正攻法ではないかもしれませんが、同じ悩みを抱える方の参考になれば幸いです。

:::note info
この記事では Swiper バージョン 11.0.5（2024年12月現在）を使用しています。
:::

## 今回作りたいもの
以下のようなスライダーを作成します。
- メインのスライダ（上部）とサムネイルのスライダ（下部）が連動する
- サムネイルは5枚のスライダを表示
- サムネイルのアクティブスライダは常に中央
- スライダはループする

![img_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115690/6d9caace-c93b-c1ad-f21c-c24d9b9141e2.png)
右矢印ボタンを押すと、メインのスライドとサムネイルが連動して動く。

## いざ、実装！
Swiper の thumbs モジュールを使用し、2つの Swiper を連動させてみます。

### thumbs についてはこちら
https://swiperjs.com/swiper-api#thumbs

### demo
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="ByBNKMa" data-pen-title="Swiper-demo01" data-user="miri-inoue" 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/miri-inoue/pen/ByBNKMa">
  Swiper-demo01</a> by Miri Inoue (<a href="https://codepen.io/miri-inoue">@miri-inoue</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


### コード
```HTML:HTML
<div class="swiper-container">
// メインのスライダ
  <div class="swiper main-swiper">
    <div class="swiper-wrapper">
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide01" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide02" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide03" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide04" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide05" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=Slide06" alt=""></div>
    </div>
  </div>
  // サムネイルのスライダ
  <div class="swiper thumbs-swiper">
    <div class="swiper-wrapper">
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=01" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=02" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=03" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=04" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=05" alt=""></div>
      <div class="swiper-slide"><img src="https://placehold.jp/dedede/595959/600x300.png?text=06" alt=""></div>
    </div>
  </div>
  <div class="swiper-button-container">
    <button class="swiper-button-prev" type="button"></button>
    <button class="swiper-button-next" type="button"></button>
  </div>
</div>
```
```SCSS:CSS
.swiper-container {
  position: relative;
  margin: 0 auto;
  width: 600px;
  height: auto;
}

.swiper-slide {
  img {
    width: 100%;
    height: auto;
  }
}

.swiper-button-prev,
.swiper-button-next {
  position: absolute;
  top: initial;
  bottom: 25px;
}

.thumbs-swiper {
  width: 600px;
}

.swiper-slide-thumb-active {
  border: 5px solid red;
}
```


```JavaScript:JavaScript
const swiper = () => {
// サムネイルにあたるスライダを先に記述します
  const thumbsSwiper = new Swiper(".thumbs-swiper", {
    centeredSlides: true,
    loop: true,
    slidesPerView: 5,
    spaceBetween: 10
  });
  const mainSwiper = new Swiper(".main-swiper", {
    slidesPerView: 1,
    loop: true,
    navigation: {
      nextEl: ".swiper-button-next",
      prevEl: ".swiper-button-prev"
    },
    thumbs: {
      swiper: thumbsSwiper
    }
  });
};

swiper();

```

### 発生した問題
- アクティブなサムネイルは最初だけ中央で表示されるが、常にセンターにはなってくれない😭
- サムネイルの表示枚数が 途中で 5枚→4枚 と変化してしまう😭


## どうやって解決したか？
メインのスライダを thumbs とすることで、「サムネイルのアクティブスライドを常に中央 × ループ」を再現することができました！

:::note warn
正攻法ではないので実装する場合には注意が必要です。
:::

### demo
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="yyBNaBx" data-pen-title="Swiper-demo02" data-user="miri-inoue" 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/miri-inoue/pen/yyBNaBx">
  Swiper-demo02</a> by Miri Inoue (<a href="https://codepen.io/miri-inoue">@miri-inoue</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


具体的にどういうこと🧐？

↓

![img_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115690/d0dcd7cd-6b59-6542-3d3e-b5eb070153e9.png)

メインのスライダとサムネイルのスライダを入れ替え、メインスライダを thumbs として設定しました。


### コード
```JavaScript:JavaScript
const swiper = () => {
  // thumbsSwiper -> mainSwiper に入れ替え
  const mainSwiper = new Swiper(".main-swiper", {
    loop: true,
    allowTouchMove: false
  });
  //  mainSwiper ->　thumbsSwiper　に入れ替え
  const thumbsSwiper = new Swiper(".thumbs-swiper", {
    centeredSlides: true,
    slidesPerView: 5,
    spaceBetween: 10,
    loop: true,
    navigation: {
      nextEl: ".swiper-button-next",
      prevEl: ".swiper-button-prev"
    },
    // thumbs に メインのスライダ
    thumbs: {
      swiper: mainSwiper
    }
  });
};

swiper();
```

## メインスライダを thumbs にする際の注意点
期待する動きになったものの、デメリットや制約があるため実装する際には以下の項目に注意してください。

### メインのスライダが1枚のときのみに有効
メインのスライドに centeredSlides: true を設定しても、アクティブなスライドはサムネイルと連動して常に中央になってくれません。

### メインのスライドをスワイプしても、サムネイルは連動しない

thumbs に設定したメインのスライドを左右にスワイプしても、メインのスライドは切り替わりますがサムネイルのスライダは連動して動きません。そのため、メインのスライダに allowTouchMove: false を設定し、スライドの動作を 「navigation button のクリック」と 「サムネイルスライダのスワイプ」に絞る必要があります。

### アクセシビリティが担保できるか検証が必要
今回の実装では、通常サムネイルスライダを設定する部分にメインのスライダを thumbs として設定しています。そのため、キーボード操作やスクリーンリーダーの読み上げが意図通りにならないといったアクセシビリティの懸念が生じる可能性があります。

- キーボード操作で全てのスライドが正しく操作できること
- スクリーンリーダーでメインコンテンツとナビゲーションが適切に認識されること

上記を満たした上で実装を行ってください。


## 試したこと・検討したこと

### CSS のカスタマイズ
サムネイルスライダを

- overflow: visible
- slidesPerView: "auto"
- centeredSlides: false

にし、

その外側の div に
- width: （（ サムネイル1枚分の width + spaceBetween ) * サムネイルを表示したい枚数）
- overflow: hidden

を設定しました。

アクティブなスライダが常に中央に表示されるものの、

- サムネイルの表示枚数が 3枚 → 4枚 → 5枚 になる
- サムネイルが連続して loop せず、右端から左端へ移動するような挙動になる

と、今回期待する挙動になりませんでした。

#### demo
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="yyBYLYg" data-pen-title="Swiper-demo-css" data-user="miri-inoue" 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/miri-inoue/pen/yyBYLYg">
  Swiper-demo-css</a> by Miri Inoue (<a href="https://codepen.io/miri-inoue">@miri-inoue</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### controller モジュールを使用して2つの Swiper を連動

https://swiperjs.com/swiper-api#controller

- loop: true
- centeredSlides: true
- サムネイルが slidesPerView: 1

の場合や

- loop: false
- centeredSlides: true
- サムネイルが slidesPerView: 5

の場合問題なく連動しますが、loop × centeredSlides × slidesPerView を掛け合わせるとうまく連動しなくなってしまいます。



#### demo

<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="dPbopJO" data-pen-title="Swiper-demo-controller" data-user="miri-inoue" 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/miri-inoue/pen/dPbopJO">
  Swiper-demo-controller</a> by Miri Inoue (<a href="https://codepen.io/miri-inoue">@miri-inoue</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### Slick
2017年に v1.8.0 がリリースされて以降、長らく更新されていないため選択肢から外しました。（2024年12月時点）

## さいごに
Swiper は手軽なライブラリではありますが、今回の経験を通して複雑な実装の場合は 公式のAPI を使用していても特定の組み合わせで期待通りの動きにならなかったり、挙動が不安定になったりとなかなか一筋縄ではいかないことを実感しました。

今回の記事で紹介した方法は正攻法とは言えないので注意が必要ですが、同じような課題に直面している方にとってヒントになれば嬉しいです！

もしこの記事が役立ったり、もっと良い方法や改善案があれば、ぜひコメントなどで共有していただけると嬉しいです！一緒により良い実装を目指していきましょう。