CSS Scroll Snapでミニマムなカルーセルスライダーをつくる

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

## はじめに
Web UIを実装する際にカルーセルスライダーを実装することはよくあることだと思います。
ライブラリをガッツリ利用するほどではないけれど、カルーセルスライダーっぽい挙動を見せたい :thinking: という想定で、[CSS Scroll Snap](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_scroll_snap)と最小限のJavaScriptで実装できるか試してみました。


## CSS Scroll Snapでできること
スライドを内包するコンテナに `scroll-snap-type`を設定するだけで、CSS Scroll Snapを利用することが可能です。
```css
scroll-snap-type: x mandatory;
```
`mandatory`は、スクロール中でなければスナップ点に合わせられる挙動をします。
（今回はx軸での移動を想定しています。）
操作感だけでいえば、これで十分です✨

## CSS Scroll Snapでできないこと
カルーセルスライダーとして利用する場合、任意のスライドに遷移したい要件も多いため、アクティブなインデックスを取得できるようにしておけるようにしたいです。
インデックスの取得はCSSでは不可能なので、JavaScriptで表示している要素を検知して実装します。


## 実装

インデックスを保持し、ボタンから該当のスライドにジャンプすることができます :dash: 
実装にはVue3を利用しています。

<p class="codepen" data-height="498" data-default-tab="result" data-slug-hash="JjqZGYg" data-pen-title="Scroll-Snap Carousel " data-user="YUUMU" style="height: 500px; 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/YUUMU/pen/JjqZGYg">
  Scroll-Snap Carousel </a> by YUU (<a href="https://codepen.io/YUUMU">@YUUMU</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

CSSは上述の通り、`scroll-snap-type: x mandatory;`を設定しています。
また、今回アクティブなスライドを中央に寄せる前提としてスライドに対して `scroll-snap-align: center;` を設定しています。 

### JavaScript ( Vue3 )
アクティブなスライドを取得するため、[IntersectionObserver](https://developer.mozilla.org/ja/docs/Web/API/IntersectionObserver)を利用します。
各スライドをそれぞれ監視対象とし、コンテナをルートとして交差したタイミングでアクティブな要素として判定します。


```javascript
const carousel = ref(); // スライドを内包する親コンテナ
const itemRefs = ref([]); // 各スライド要素が格納された配列
const index = ref(0); // スライドのインデックス

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // 可視対象の要素と一致するスライドでインデックスを更新する
      index.value = itemRefs.value.findIndex(element => {
        return element === entry.target;
      });
    }

  });
}, {
  root: carousel.value, // コンテナ
  rootMargin: "0px -50%", // 中央で交差判定を行う
});

// 各スライドを監視する
itemRefs.value.forEach((el) => {
  observer.observe(el);
});
```

その上で各スライドの幅を元にしてコンテナ内をx軸方向にスクロールする `slideToIndex()` を実装することで、シンプルにスムースな遷移が設定できました 😺
もちろん、インデックスを利用することで、前/次のスライドに遷移するボタンなども作ることが可能です。
```javascript
const scrollToIndex = (index) => {
  const itemWidth = 300; // スライドの幅を指定
  const scrollPosition = index * itemWidth;
  carousel.value.scrollTo({
    left: scrollPosition,
    behavior: 'smooth'
  });
}
```

### 各スライドの幅をそれぞれ変更してみる
スライドの幅をそれぞれ変更する場合も同様にインデックスを保持することができます。
スライドにジャンプする際は、幅とマージンを加味して計算することで意図した位置まで移動します :dash: 
注意点としては、幅の小さめのスライドが存在する場合は、意図せぬスライドまでスナップしてしまうため、中央に寄せるための調整が必要です。

<p class="codepen" data-height="500" data-default-tab="js,result" data-slug-hash="qBGKxXm" data-pen-title="Scroll-Snap Carousel Various Width" data-editable="true" data-user="YUUMU" style="height: 500px; 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/YUUMU/pen/qBGKxXm">
  Scroll-Snap Carousel Various Width</a> by YUU (<a href="https://codepen.io/YUUMU">@YUUMU</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## まとめ
- シンプルな実装でカルーセルスライダーを実装することができました😺 ループ不要でアニメーション等にこだわる必要がない場合はよさそうです
- 単なる横スクロールなので各スライドへのジャンプはわかりやすく実装できます :thumbsup: 
- `scroll-snap-align` のプロパティ変更を行う場合、IntersectionObserverのoptionsを調整する必要がありそうです（今後検証してみます） 

## 一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。