MicromodalとSplideでアクセシブルなカルーセル付きモーダルを実装する

MicromodalとSplideでアクセシブルなカルーセル付きモーダルを実装する

MicromodalとSplideを使用してカルーセル付きのモーダルを実装した際の備忘録を残しておきます。
使用言語、ツール、仕様は以下です。

  • HTML
  • CSS
  • JavaScript
  • markuplint (lintツール、WAI-ARIA実装時のチェックに使用)
  • Micromodal.js(モーダルの実装)
  • Splide(カルーセルスライダーの実装)

Micromodalの導入

Micromodal.js」は、jQueryに依存しない、Vanilla.js製のモーダルライブラリです。
軽量かつ、アクセシビリティにも対応しているのが特徴です。
npmやyarnでインストールするか、CDNでscriptを読み込みます。

npm

$ npm install micromodal --save

yarn

$ yarn add micromodal --save

cdn

<script src="https://cdn.jsdelivr.net/npm/micromodal/dist/micromodal.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>

Micromodalのメリット

Micromodal.jsには以下のようなメリットがあります。

  • jQuery等に依存していない。
  • 1.9KBと軽量。
  • デフォルトのスタイルがないため、カスタマイズ性が高い。

また、WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications)などW3Cの仕様の中にあるモーダルダイアログのガイドラインに準拠しています。以下のような項目をクリアすることができます。

  • モーダルが開いている間はモーダル内のコンテンツのみにフォーカスがあたるするようにする。
  • モーダルが開いている間は後ろのコンテンツを操作することはできない。
  • モーダルが開いたら、フォーカスをモーダル内の最初のフォーカス可能な要素に移動する。
  • escキーでモーダルを閉じることができる。

Micromodalの参考記事
【Vanilla JS】軽量かつシンプルなモーダルプラグイン『Micromodal.js』

公式
Micromodal.js

Splideの導入

Splide
同じくnpmやyarnでインストールするか、CDNでscriptを読み込みます。

npm

$ npm install @splidejs/splide

yarn

$ yarn add @splidejs/splide

cdn

<!-- css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@latest/dist/css/splide.min.css">
<!-- js -->
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@latest/dist/js/splide.min.js"></script>

Splideのメリット

Splideには以下のような特徴があります。

  • jQuery等に依存していない。
  • 29KBと軽量
  • ドキュメントが日本語
  • アクセシビリティ対応に力を入れている

以下の公式ドキュメントにてアクセシビリティに関して詳しく解説されています。

アクセシビリティ – splide

Splideの参考記事
最新バージョン4】Splideの使い方を徹底解説!-14個のデモ付き-

公式
Splide.js

デモ

以下がMicromodalとSplideでカルーセル付きモーダルを実装したデモです。

HTML

モーダルを開くボタンなどに、「data-micromodal-trigger」を付与し、値として対象のモーダルを指定します。モーダルを閉じる要素(背景や閉じるボタンなど)には、「data-micromodal-close」を付与します。

  <!-- モーダルボタン -->
  <ul class="p-modal-buttons">
    <li class="p-modal-buttons__item">
      <button type="button" data-micromodal-trigger="modal-1" data-index="0" class="js-modal-button p-modal-buttons__button">
        <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample01.png" alt="" decoding="async" width="600" height="600">
        <span class="p-modal-buttons__text">modal01</span>
      </button>
    </li>
    <li class="p-modal-buttons__item">
      <button type="button" data-micromodal-trigger="modal-1" data-index="1" class="js-modal-button p-modal-buttons__button">
        <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample02.png" alt="" decoding="async" width="600" height="600">
        <span class="p-modal-buttons__text">modal02</span>
      </button>
    </li>
    <li class="p-modal-buttons__item">
      <button type="button" data-micromodal-trigger="modal-1" data-index="2" class="js-modal-button p-modal-buttons__button">
        <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample03.png" alt="" decoding="async" width="600" height="600">
        <span class="p-modal-buttons__text">modal03</span>
      </button>
    </li>
  </ul>
  <!-- / モーダルボタン -->


  <!-- モーダルコンテンツ & スライダー -->
  <div class="p-modal p-micromodal-slide" id="modal-1" aria-hidden="true">
    <div class="p-modal__overlay" tabindex="-1" data-micromodal-close>
      <div class="p-modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
        <header class="p-modal__header">
          <h2 class="p-modal__title" id="modal-1-title">
            Micromodal &amp; Splide demo
          </h2>
          <button class="p-modal__close" aria-label="Close modal" data-micromodal-close></button>
        </header>
        <main class="p-modal__content" id="modal-1-content">
          <div class="splide js-slider" role="group" aria-label="Splide テストスライダー">
            <div class="splide__track">
              <ul class="splide__list">
                <li class="splide__slide">
                  <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample01.png" alt="" decoding="async" width="600" height="600">
                </li>
                <li class="splide__slide">
                  <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample02.png" alt="" decoding="async" width="600" height="600">
                </li>
                <li class="splide__slide">
                  <img src="https://kt-media.blog/wp-content/uploads/2023/01/sample03.png" alt="" decoding="async" width="600" height="600">
                </li>
              </ul>
            </div>
          </div>
        </main>
      </div>
    </div>
  </div>
  <!-- / モーダルコンテンツ & スライダー -->

JavaScript

SplideとMicromodalの初期化、Optionの定義。
加えて、モーダルボタンクリック時にボタンの番号(data-index属性の値)をスライダーへ保存する処理を加えています。

/*
  Splideの初期化
  Optionの定義
*/
const sliderElement = document.querySelector('.js-slider');
const slideSpeed = 400;
const slider = new Splide(sliderElement, {
  type: 'fade',// スライダーのタイプ、fadeを選択
  rewind: true,// スライダーの終わりまで行ったときに、先頭に巻き戻せるかどうか
  speed: slideSpeed,// スライダーの移動時間をミリ秒単位で指定
}).mount();


/*
  モーダルボタンの設定
  ボタンの番号をスライダーへ保存する
*/
const modalButtons = document.querySelectorAll('.js-modal-button');
modalButtons.forEach(modalButton => {
  modalButton.addEventListener('click', () => {
    sliderElement.dataset.showIndex = modalButton.dataset.index;
  });
});


/*
  Micromodalの初期化
  Optionの定義
*/
MicroModal.init({
  disableScroll: true,
  onShow: () => {
    slider.options = {
      speed: 0// アニメーション時間を0に
    };
    // 保存したボタンの番号のスライドへ移動
    slider.go(Number(sliderElement.dataset.showIndex));
    slider.options = {
      speed: slideSpeed//アニメーション時間を戻す
    };
  }
});

MicroModalのonShowメソッドはモーダルが開いたときに、実行される関数です。
1つ目のパラメータにモーダルオブジェクトを、2つ目のパラメータに実行したい内容を指定します。

CSS

Micromodalプラグイン側で操作している部分は、aria-hiddenの切り替えclassの付け外ししかありません。モーダルを出現させる動きについては、css側でコントロールしています。
プラグイン既存のcssがない分カスタマイズ性が高いのがメリットです。

以下はサンプルです。

/* base styles */
h1 {
  text-align: center;
  font-size: 32px;
  margin-top: 40px;
  line-height: 1.4;
}
small {
  display: block;
  font-size: 60%;
}

body {
  width: 100%;
  margin: 0;
  padding: 0;
  color: #000;
  background-color: #fff;
  font-size: 14px;
  line-height: 1.7em;
  font-weight: 400;
  font-family: -apple-system,"BlinkMacSystemFont","Hiragino Kaku Gothic ProN","Hiragino Sans",Meiryo,sans-serif,"Segoe UI Emoji";
}

/* MicroModal Styles */
.p-modal {
  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
}

.p-modal__overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  justify-content: center;
  align-items: center;
}

.p-modal__container {
  background-color: #fff;
  padding: 30px;
  max-width: 500px;
  max-height: 100vh;
  border-radius: 4px;
  overflow-y: auto;
  box-sizing: border-box;
}

.p-modal__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.p-modal__title {
  margin-top: 0;
  margin-bottom: 0;
  font-weight: 600;
  box-sizing: border-box;
}

.p-modal__close {
  background: transparent;
  border: 0;
}

.p-modal__header .p-modal__close:before { 
  content: "\2715"; 
  text-align: center;
  background-color: #ccc;
  padding: 5px 10px;
  border-radius: 50%;
  font-size: 20px;
  cursor: pointer;
}

.p-modal__content {
  margin-top: 24px;
  line-height: 1.5;
  color: rgba(0,0,0,.8);
}

.p-modal__btn {
  background-color: #e6e6e6;
  color: rgba(0,0,0,.8);
  border-style: none;
  border-width: 0;
  cursor: pointer;
  text-transform: none;
  overflow: visible;
  line-height: 1.15;
  margin: 0;
  will-change: transform;
  -moz-osx-font-smoothing: grayscale;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
  transition: -webkit-transform .25s ease-out;
  transition: transform .25s ease-out;
  transition: transform .25s ease-out,-webkit-transform .25s ease-out;
}

.p-modal__btn:focus, .p-modal__btn:hover {
  -webkit-transform: scale(1.05);
  transform: scale(1.05);
}

.p-modal__btn-primary {
  background-color: #00449e;
  color: #fff;
}



/* animation styles */
@keyframes mmfadeIn {
    from { opacity: 0; }
      to { opacity: 1; }
}

@keyframes mmfadeOut {
    from { opacity: 1; }
      to { opacity: 0; }
}

@keyframes mmslideIn {
  from { transform: translateY(15%); }
    to { transform: translateY(0); }
}

@keyframes mmslideOut {
    from { transform: translateY(0); }
    to { transform: translateY(-10%); }
}


/* slider */
.p-micromodal-slide {
  display: none;
}

.p-micromodal-slide.is-open {
  display: block;
}

.p-micromodal-slide[aria-hidden="false"] .p-modal__overlay {
  animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.p-micromodal-slide[aria-hidden="false"] .p-modal__container {
  animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
}

.p-micromodal-slide[aria-hidden="true"] .p-modal__overlay {
  animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.p-micromodal-slide[aria-hidden="true"] .p-modal__container {
  animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
}

.p-micromodal-slide .p-modal__container,
.p-micromodal-slide .p-modal__overlay {
  will-change: transform;
}

.splide__pagination__page.is-active {
  background-color: #6C63FF;
}

/* modal-button styles */
.p-modal-buttons {
  display: flex;
  list-style: none;
  padding: 0;
  margin-top: 32px;
  flex-direction: column;
  width: 90%;
  max-width: 1280px;
  margin-left: auto;
  margin-right: auto;
  row-gap: 32px;
}
.p-modal-buttons__item {
  width: 100%;
}

.p-modal-buttons__button {
  padding: 24px;
  border: none;
  background-color: #e7e7e7;
  border-radius: 3px;
}
.p-modal-buttons__text {
  display: block;
  font-size: 16px;
  margin-top: 12px;
}
img {
  height: auto;
  max-width: 100%;
  vertical-align: top;
}


/* responsive styles */
@media screen and (min-width: 768px) {
  .p-modal-buttons {
    gap: 3%;
    flex-direction: row;
    justify-content: center;
  }

  .p-modal-buttons__item {
    width: calc(33.3% - 3% * 2 / 3);
  }
}