BLOG

Embla Carouselを使ってカルーセルを作る

Written by koizumi

INDEX

カルーセルUIを実装する際はSwiperやSplideといったライブラリを使うことが多いかと思います。
これらのライブラリは独自のCSSの読み込みが必要であったり、バンドルサイズの肥大化によってパフォーマンスが低下したりといったデメリットが存在しますが、Embla Carouselというシンプルかつ軽量なライブラリを使用することで、そういったデメリットを気にせずカルーセルUIを実装することができます。

今回はEmbla Carouselを使ったカルーセルUIの作り方を紹介いたします。

※本記事の内容は執筆時点で最新の安定版であるv8に基づいて解説しております

Embla Carouselとは

軽量で依存関係がなく、フレームワークに依存しないカルーセルライブラリです。

後発のカルーセルライブラリということもあり、他ライブラリと比較して次のようなメリットを兼ね備えています。

公式サイト:https://www.embla-carousel.com/

高い拡張性・柔軟性

実装したい機能に合わせてオプションやプラグイン等を追加することで、カルーセルをカスタマイズすることができます。
また、独自のCSSも存在せず、ユーザー自身で柔軟にスタイリングすることが可能です。

軽量なバンドルサイズ

他ライブラリでは全ての機能が初めから組み込まれているのに対して、Embla Carouselでは必要な機能だけを後付けで追加していくため、バンドルサイズが肥大化せず、非常に軽量なライブラリになっています。

様々なフレームワークへの対応

純粋なJavaScriptだけでなく、ReactやVue, Svelte, Solidなどの主要なフレームワークにも対応しています。
公式ドキュメントにはそれぞれのフレームワークに対してのセットアップ方法も記載してあり、スムーズに導入できます。

そのほか、アクセシビリティ対策については、Splideが標準でアクセシビリティに配慮した作りになっているのに対し、Embla Carouselはv9でアクセシビリティプラグインが追加されました。
他のプラグインと同様、必要な機能だけを追加することができます。

シンプルなカルーセルを実装する

公式のガイド(v8版)を参照し、Vanilla JSを使ったシンプルなカルーセルの実装方法について解説していきます。

1.インストール

npmもしくはCDNを使ってEmbla Carouselをプロジェクトに含めます。

npm

npm install embla-carousel --save

CDN

<script src="https://unpkg.com/embla-carousel/embla-carousel.umd.js"></script>

2.デフォルト機能の実装

まずはスライド表示とスワイプ/ドラッグ操作といった最小限の機能のみが備わったシンプルなカルーセルを実装します。

HTML

<div class="embla">
  <div class="embla__viewport">
    <div class="embla__container">
      <div class="embla__slide">
        <img src="https://picsum.photos/id/237/500/300" width="300" height="200" alt="">
      </div>
      <div class="embla__slide">
        <img src="https://picsum.photos/id/137/500/300" width="300" height="200" alt="">
      </div>
      <div class="embla__slide">
        <img src="https://picsum.photos/id/337/500/300" width="300" height="200" alt="">
      </div>
    </div>
  </div>
</div>

マークアップは以下の構成に従う必要があります。

  • はみ出したコンテンツを非表示にするオーバーフロー・ラッパー(上記コードのembla__viewport
  • スライドを格納し、スクロールさせるスクロール・コンテナ(embla__container
  • 1つ以上のスライド(embla__slide

HTML構造が合っていればクラス名は自由につけることが可能です。

emblaラッパーはスタイリングやスクリプトの設定のために設置していますが、必須ではありません。

CSS

.embla {
  --slide-size: 100%;

  max-width: 500px;
  margin-inline: auto;
}

.embla__viewport {
  overflow: hidden;
}

.embla__container {
  display: flex;
}

.embla__slide {
  flex: 0 0 var(--slide-size); /* カルーセルの全幅 */
  min-width: 0;
}

.embla__slide img {
  width: 100%;
}

上記のうち、viewport要素へのoverflow: hiddenの設定は必須です。
設定しない場合、スライドがビューポートからはみ出してしまい正しくスクロールが動作しません。

JS

import EmblaCarousel from 'embla-carousel' /* Embla Carousel のインポート */

const wrapperNode = document.querySelector('.embla')
const viewportNode = wrapperNode.querySelector('.embla__viewport')
const emblaApi = EmblaCarousel(viewportNode)

embla__viewportクラスを持つ要素を選択し、それをEmblaCarouselコンストラクタの最初の引数として渡します。
これにより、カルーセルが初期化され、Embla Carousel APIの機能を利用できるようになります。

上記の記述のみで、最小限の機能が備わったシンプルなカルーセルを実装できます。

3.余白の調整

CSSからスライド間の余白を調整します。

公式ガイドではパディングを使用した方法が推奨されているため、今回はその方法で調整をします。

.embla {
  --slide-size: 100%;
 --slide-spacing: 10px;

  max-width: 500px;
  margin-inline: auto;
}

/* 省略 */

.embla__container {
  display: flex;
  margin-left: calc(var(--slide-spacing) * -1); /*  スライドのパディング分(10px)、コンテナに負のマージンを設定 */
}

.embla__slide {
  flex: 0 0 var(--slide-size); /* カルーセルの全幅 */
  min-width: 0;
  padding-left: var(--slide-spacing); /*  スライドのパディング */
}

スライド自体にパディングを設定し、コンテナにそれに対応する負のマージンを設定することでスライド間の余白を作成しています。

4.前へ&次へボタンの実装

カルーセルを操作するためのボタンを追加します。

HTMLでボタン要素を作成し、EmblaのナビゲーションメソッドであるscrollPrevscrollNextに接続してください。

HTML

<div class="embla">
  <!-- ルート要素 -->
  <div class="embla__viewport">
    <div class="embla__container">
      <div class="embla__slide">
        <img src="https://picsum.photos/id/237/500/300" width="300" height="200" alt="">
      </div>
      <!-- 省略 -->
    </div>
  </div>

  <!-- ボタンはルート要素の外側に記述 -->
 <button class="embla__button embla__prev">prev</button>
  <button class="embla__button embla__next">next</button>
  
</div>

デフォルトの設定では、ルート要素(EmblaCarouselの初期化の際に渡される要素)はポインタイベントに応答します。
クリックした際に意図しないドラッグ操作が発生するのを防ぐため、ボタンはルート要素の外側に配置します。

JS

import EmblaCarousel from 'embla-carousel'

const wrapperNode = document.querySelector('.embla')
const viewportNode = wrapperNode.querySelector('.embla__viewport')
const emblaApi = EmblaCarousel(viewportNode)

const prevButtonNode = wrapperNode.querySelector('.embla__prev') /* 「前へ」ボタン */
const nextButtonNode = wrapperNode.querySelector('.embla__next') /* 「次へ」ボタン */

prevButtonNode.addEventListener('click', () => emblaApi.scrollPrev(), false)
nextButtonNode.addEventListener('click', () => emblaApi.scrollNext(), false)

/* スライドが端に達した時にボタンを無効化 */
const toggleButtonsDisabled = (emblaApi) => {
  const setButtonState = (button, enabled) => {
    button.toggleAttribute('disabled', !enabled)
  }
  setButtonState(prevButtonNode, emblaApi.canScrollPrev())
  setButtonState(nextButtonNode, emblaApi.canScrollNext())
}

toggleButtonsDisabled(emblaApi)
emblaApi.on('select', toggleButtonsDisabled) /* スライドが切り替わったとき */
emblaApi.on('reinit', toggleButtonsDisabled) /* カルーセルが初期化されたとき */

「前へ」「次へ」ボタンがクリックされたときに、APIのメソッド(scrollPrev, scrollNext)を呼び出してスライドを移動させます。

また、toggleButtonsDisabledの処理で、スライドが最初や最後に達したときにボタンを押せないようにしています。

CSS

.embla__button {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  position: absolute;
  top: 50%;
  display: flex;
  justfy-content: center;
  arign-item: center;
  border: none;
  transition: all ease 0.2s;
  cursor: pointer;
}

.embla__prev {
  left: 0;
  translate: -50% -50%;
}

.embla__next {
  right: 0;
  translate: 50% -50%;
}

.embla__prev:disabled,
.embla__next:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

ボタンの位置や形状をCSSで調整します。
ボタンが無効になった際に透過するように設定しています。

ボタンを追加したサンプルが以下になります。

5.ドットボタンの実装

スライドの枚数や位置を表示するドットボタンを追加します。

HTML

<div class="embla">
  <!-- ルート要素 -->
  <div class="embla__viewport">
    <div class="embla__container">
      <div class="embla__slide">
        <img src="https://picsum.photos/id/237/500/300" width="300" height="200" alt="">
      </div>
      <!-- 省略 -->
    </div>
  </div>

  <!-- ドットはルート要素の外側に記述 -->
 <div class="embla__dots"></div>
 
  <script type="text/template" id="embla-dot-template">
    <button class="embla__dot">
    </button>
  </script>
  
</div>

前項のボタンと同様に、クリックした際に意図しないドラッグ操作が発生するのを防ぐため、ドットはルート要素の外側に配置します。

JS

import EmblaCarousel from 'embla-carousel'

const wrapperNode = document.querySelector('.embla')
const viewportNode = wrapperNode.querySelector('.embla__viewport')
const emblaApi = EmblaCarousel(viewportNode)
const prevButtonNode = wrapperNode.querySelector('.embla__prev')
const nextButtonNode = wrapperNode.querySelector('.embla__next')

const dotsNode = wrapperNode.querySelector('.embla__dots') /* ドットボタン */

/* 省略 */

let dotNodes = []

/* スライドの総数を取得し、ドットを生成 */
const createDotButtonHtml = (emblaApi, dotsNode) => {
  const dotTemplate = document.getElementById('embla-dot-template')
  const snapList = emblaApi.scrollSnapList()
  dotsNode.innerHTML = snapList.reduce((acc) => acc + dotTemplate.innerHTML, '')
  return Array.from(dotsNode.querySelectorAll('.embla__dot'))
}

/* クリックしたドットのスライドへ移動 */
const addDotButtonClickHandlers = (emblaApi, dotNodes) => {
  dotNodes.forEach((dotNode, index) => {
    dotNode.addEventListener('click', () => emblaApi.scrollTo(index), false)
  })
}

/* 選択中のスライドを示すドットにクラスを追加 */
const toggleDotButtonsActive = (emblaApi, dotNodes) => {
  if (!dotNodes.length) return
  const previous = emblaApi.previousScrollSnap()
  const selected = emblaApi.selectedScrollSnap()
  dotNodes[previous].classList.remove('embla__dot--selected')
  dotNodes[selected].classList.add('embla__dot--selected')
}

const createAndSetupDotButtons = (emblaApi, dotsNode) => {
  dotNodes = createDotButtonHtml(emblaApi, dotsNode)
  addDotButtonClickHandlers(emblaApi, dotNodes)
  toggleDotButtonsActive(emblaApi, dotNodes)
}

createAndSetupDotButtons(emblaApi, dotsNode)
emblaApi.on('reinit', () => createAndSetupDotButtons(emblaApi, dotsNode))
emblaApi.on('select', (emblaApi) => toggleDotButtonsActive(emblaApi, dotNodes))

Embla Carouselは、scrollSnapListメソッドを通じてスライドの総数を公開します。これを利用して、各スライドに対応するドットボタンを動的に生成できます。

ドットをクリックすると、scrollTo(index)が呼ばれ、対応するスライドへ移動するようになっています。

また、表示されているスライドに対応するドットの見た目を変えるため、特定のクラスを付ける処理を追加しています。

CSS

.embla__dots {
  display: flex;
  justify-content: center;
  column-gap: 10px;
  margin-top: 10px;
}

.embla__dot {
  background-color: #dddddd;
  border: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  padding: 0;
  cursor: pointer;
}

.embla__dot--selected {
  background-color: #333333;
}

ドットの位置や間隔、色等をCSSで調整します。

これでドットが追加され、基本的な機能が備わったカルーセルが完成しました。

オプションを設定する

Embla Carouselには、動作を制御するためのさまざまな設定オプションが用意されています。

コンストラクタ経由で設定するか、グローバルなデフォルト値として設定できます。
両方が指定され競合している場合、コンストラクタのオプションが優先されます。

全てのオプションとデフォルト値については公式ページを参照ください。

コンストラクタオプション

コンストラクタオプションはカルーセルの起動時に設定します。 下記の例では、loopオプションをtrueに設定しています。

import EmblaCarousel from 'embla-carousel'

const wrapperNode = document.querySelector('.embla')
const viewportNode = wrapperNode.querySelector('.embla__viewport')

/* loopをtrueに設定 */
const emblaApi = EmblaCarousel(viewportNode, { loop: true })

2枚のスライドをループ表示させる場合の注意点

スライドが2枚の状態で loop: true を設定すると、「2枚目が中途半端な位置で止まる」「ループが一周しない」といった挙動が発生することがあります。
これはEmbla Carouselのスクロール範囲の制限(containScroll)が干渉しているためです。

この現象が発生した場合は、オプションに containScroll: false を追加することで解決できます。

const emblaApi = EmblaCarousel(viewportNode, {
  loop: true,
  containScroll: false
})

グローバルオプション

グローバルオプションを設定すると、すべてのカルーセルにその設定が適用され、Emblaのデフォルト設定がユーザーの設定で上書きされます。
下記の例では、loopオプションをtrueに設定しています。

import EmblaCarousel from 'embla-carousel'

/* グローバルオプション */
EmblaCarousel.globalOptions = { loop: true }

const wrapperNode = document.querySelector('.embla')
const viewportNode = wrapperNode.querySelector('.embla__viewport')

const emblaApi = EmblaCarousel(viewportNode, { align: 'start' })

プラグインを追加する

Embla Carouselはプラグインを追加することで機能の追加やカルーセルのカスタマイズができます。
公式プラグインはすべて個別のNPMパッケージとして公開されているため、必要なものをそれぞれインストールして使用します。

ここではプラグインを追加して機能をカスタマイズしたカルーセルのサンプルを紹介します。

自動再生

自動スクロール

フェード

その他のプラグインやAPIを使用したカルーセルのサンプルは公式ページを参照ください。

おわりに

Embla Carouselを使ってカルーセルを実装してみましたが、CSSで柔軟にスタイリングできる点が個人的に魅力的に感じました。
また、公式ページには様々なパターンのサンプルがあるだけでなく、SandBox からコード例を参照することもできるので、学習のハードルがそこまで高くないことも良い点だと感じます。

スライド切り替え時のアニメーションなどを細かく調整することは執筆時点ではできないようですので、そういった点は他ライブラリに劣りますが、パフォーマンス重視のプロジェクトでシンプルなカルーセルを実装する場合は十分力を発揮できるライブラリになっています。

要件や開発環境が合う場合はカルーセルライブラリの選択肢の一つとして検討してみてはいかがでしょうか。

参考サイト

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

https://zenn.dev/newfolk/articles/8a9e289329f9bd

https://blog.to-ko-s.com/articles/embla-carousel