🎠

Next.jsでカルーセル/スライダーを実装するならEmbla Carouselがおすすめ

2023/09/21に公開

快適なカルーセル/スライダーライブラリを求めて彷徨っていましたが、いい感じのものを見つけたので紹介します。

開発環境

  • Next.js v13.4.19
  • npm v9.5.1
  • node v18.16.0
  • Embla Carousel React v8.0.0-rc14

Embla Carouselとは

流れるような動きと正確なスワイプを実現した、シンプルなカルーセルライブラリです(本文直訳)。他のライブラリに依存しておらず、100%オープンソースです。

HP
https://www.embla-carousel.com/

GitHub
https://github.com/davidjerleke/embla-carousel

良さそうな点

バンドルサイズが軽量

v8.0.0rc-14はMINIFIED+GZIPPEDで6.8kBです。カルーセルライブラリで有名なSwiperは同条件で17.87kB(v10.2.0)なので、半分以下のサイズです。

メンテナンスが続いている/頻繁に行われている

執筆中(2023-09-21)に確認したところ、最終更新は5時間前でした。直近のRelease Noteからも、頻繁に更新されていることがわかります。

version release date
v8.0.0-rc14 2023-09-20
v8.0.0-rc13 2023-09-16
v8.0.0-rc12 2023-08-22
v7.1.0 2023-03-05

すべてのモダンブラウザに対応

IEのサポートが終了してからは気にする機会も減りましたが、それでもたまに「Safariで意図した挙動をしない」のようなことはあるので、対応が保証されていると安心できます。

多くのフレームワークに対応

React/Vue/Svelteに対応しています。vanilla JS(module)やCDNでも使用できるので、ほとんどの環境に導入できると思います。

TypeScriptに対応

TypeScriptの恩恵は様々ですが、特にライブラリがTypeScriptに対応している場合optionをカスタマイズする際にエディタによる補完を受けることができる点だけでも開発体験が大きく上がる感覚があります。

安定して動作する

案外これが理由で導入を見送ることも多く、今回調査している過程でも「良さそうだったので導入してみたものの、いざ実装してみるとスライドやスワイプ時の挙動が不安定」というケースに何度か遭遇しました。

デモページを触れば事前に発見できる場合がほとんどなので、あらかじめ確認しておきましょう(自戒)。Embla Carouselは今のところ安定して綺麗に動作してくれています。

レイアウト・デザインの実装に対して柔軟

少し触ってみた段階での感想ですが、Embla Carouselはカルーセルを実装する際のライブラリから受けるレイアウト・デザイン面での制約がかなり少ないように感じました。

経験則として、カルーセルライブラリ上でのレイアウト・デザインの実装はカルーセル領域の計算方法や計算タイミングに関連するところで課題が挙がりやすいイメージがあります。

わかりやすいところだと

  • 「widthが異なるスライドを取り扱いたい」「スワイプした際の表現をリッチにしたい」といったやや特殊な対応が難しい/対応できない

    • ライブラリのカルーセル領域の計算の仕様上対応できないケース
  • レスポンシブ対応が難しい/対応できない

    • カルーセルのレイアウトがJavaScriptで実現されており、その計算タイミングがload時なケース

あたりでしょうか。上記は技術選定時点で回避できる課題ですが、こうした仕様は技術選定後に追加される場合も少なくなく、そのタイミングで課題になりやすいです。他には「既に導入されているカルーセルライブラリを別のデザインで流用したい場合」とか。

また、上記とまではいかずとも

  • 「これくらいのデザイン対応なら問題ないだろう」と思っていたのにいざ実装してみると微妙にうまくいかない
    • 実現できなくはないけど実装に若干工夫が必要なケース

のような場合も多い気がします。実際にはこのレベルの対応が積み重なった結果複雑になっていくパターンが多い印象[1]

一方で、Embla CarouselはIt aims to solve the hardest technical challenges with building carousels引用元)ともある通りカルーセル実装における技術的な課題の解決に重きが置かれており、所望のレイアウト・デザインの実現に対してもかなり柔軟に対応できるようになっているので、「ライブラリの都合上シンプルに実現できないレイアウト・デザインを工夫して実現した結果、実装がどんどん複雑になっていく」パターンに陥りにくい印象を受けました。

サンプルが豊富

ライブラリ内に特定のデザインが用意されているわけではないので自分でcssを書いていく必要がありますが、公式のExampleが豊富なのでこれをベースにすることで簡単にカルーセルを実装することができます。

また、所望のカルーセルのコードを出力するGenerator機能も存在しています(2023-09-21時点ではexperimental)。

気になっている点(App Router上での実装に対して)

これはライブラリに対する懸念というよりApp Router(Client Component)上での実装における不明点なのですが、optionによってはJavaScriptの適用によるカルーセルレイアウトの実現前後のチラつきが見えてしまう点が気になっています。

たとえばOpacityのExampleですが、これをClient Component上で実装すると

  1. スライドが表示される
    この時点では最初のスライドの左には何も存在しないので左揃えで表示される
  2. useEmblaCarousel(中身はuseEffect)によってoption({loop: true})が適用される
    これにより最初のスライドの左に最後のスライドが表示されるようになる
  3. 2の結果を受けて最初のスライドが左揃えから中央揃えになる
    この挙動がユーザーにチラつきとして見えてしまう

のような挙動になってしまいます。

ブラウザレンダリング完了までの流れとしては理解できるのですが、Example上ではこのような挙動にはなっていないのでClient Component上での回避方法を探しています。

一応ブラウザレンダリング完了後にカルーセルを表示するような実装(下記におけるisShow)で回避できなくはないのですが、これはベストプラクティスではない気がします...。

src\components\EmblaCarousel\EmblaCarousel.tsx
"use client"; // for App Router

import React, { useEffect, useState } from "react";
import useEmblaCarousel, { EmblaOptionsType } from "embla-carousel-react";
import "./styles.scss";
import Image from "next/image";

const options: EmblaOptionsType = {
  loop: true,
};

type Props = {
  images: Image[];
};

export const EmblaCarousel = (props: Props) => {
  const { images } = props;
  const [isShow, setIsShow] = useState(false);
  const [emblaRef] = useEmblaCarousel(options);

  useEffect(() => {
    setIsShow(true);
  }, []);

  return (
    isShow && (
      <div className="embla">
        <div className="embla__viewport" ref={emblaRef}>
          <div className="embla__container">
            {images.map((image) => (
              <div className="embla__slide" key={image.alt}>
                <Image
                  className="embla__slide__image"
                  src={image.url}
                  alt={image.alt}
                  fill
                  priority
                />
              </div>
            ))}
          </div>
        </div>
      </div>
    )
  );
};

おわりに

導入手順については公式のGet Startedの通りに進めて特に躓かなかったので省略しました。

脚注
  1. 上記の課題に関しても完全に対応できない場合は少なく、たとえばレスポンシブは「ブレークポイント毎に別のカルーセルを実装してそれを出し分ける(※厳密にはこれはレスポンシブではない)」や「画面幅が変化した際にカルーセル領域を再計算させる」等で実現できはするのですが、個人的にはライブラリレベルで柔軟に対応できるような仕組みになっていてほしい気持ちがあります。 ↩︎

Discussion