Swiperのページネーションにサークルアニメーションを実装する方法

Swiperのページネーションにサークルアニメーションを実装する方法

JavaScriptのスライダーライブラリであるSwiperにおいて、ページネーションをカスタマイズする方法をご紹介しています。

今回は、ページネーションに円形のプログレスバーのアニメーションを追加し、視覚的に次のスライドまでの時間が把握できるようにしてみました。

動作確認環境:

swiper@10

目次

Swiperのページネーションに円形のアニメーションを実装した例

今回実装したのはこちらです。

円形のページネーションの周りを、プログレスバーが各スライドごとにアニメーションするスライダーです。

大きく分けて下記の2つの実装方法が考えられます。

Swiperのページネーションに円形のアニメーションを実装する2つの方法
  • 擬似要素で円の外周を表現する方法
  • 円のSVGを用意してアニメーションさせる方法

それぞれメリット・デメリットを把握した上で、最適な方法で実装すると良いかと思います。2つの方法についてご紹介します。

擬似要素で実装する方法

先ほどご紹介したデモサイトでは、擬似要素を使いアニメーションを実装しています。

仕組みはシンプルで、ページネーションの左右に擬似要素を配置し、CSSのkeyframesで動かしてプログレスバーを演出しています。

こちらのブログ記事を参考にさせていただきました。

参考サイト:https://freefuntimes.com/?p=808

コードの見本

SwiperをCDNの形式でindex.html内で読み込んでいます。

headタグ内

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min.css" />

bodyタグの終了直前

<script src="https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min.js"></script>

index.html

<div class="wrapper">
    <div class="swiper circlePaginationSlider">
        <div class="swiper-wrapper">
            <div class="swiper-slide"><img src="./images/06.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/07.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/08.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/09.jpg" alt="サンプル画像"></div>
        </div>
    </div>
    <div class="swiper-pagination"></div>
</div>

style.css

/* ==========================
  Swiperのスタイルを調整
========================== */
.wrapper {
  position: relative;
}

.swiper-slide {
  height: auto;
}

.swiper-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ==========================
  ページネーションの間隔を調整
========================== */
.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet {
  margin: 0 5px;
}

/* ==========================
  円形のページネーションを作成
========================== */
.circle-pagination {
  position: relative;
  width: 40px;
  height: 40px;
  z-index: 1;
  background-color: #222;
  border-radius: 50%;
  text-align: center;
  overflow: hidden;
  margin: auto;
  cursor: pointer;
  font-size: 14px;
  opacity: 1;
}

.circle-pagination .circle-pagination_inner {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 35px;
  height: 35px;
  background-color: #fff;
  border-radius: 50%;
  color: #BFBFBF;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
}

.swiper-pagination-bullet.swiper-pagination-bullet-active .circle-pagination_inner {
  color: #222;
}

/* ==========================
  擬似要素でプログレスバーを作成
========================== */
.circle-pagination::before {
  content: "";
  display: block;
  width: 40px;
  height: 40px;
  background-color: #fff;
  transform-origin: right 20px;
  position: absolute;
  top: 0;
  left: -20px;
  z-index: 2;
}

.circle-pagination::after {
  content: "";
  display: block;
  width: 40px;
  height: 40px;
  background-color: #fff;
  transform-origin: left 20px;
  position: absolute;
  top: 0px;
  left: 20px;
  z-index: 3;
}

.swiper-pagination-bullet.swiper-pagination-bullet-active.circle-pagination::before {
  animation: circleRight 5s linear forwards;
}

.swiper-pagination-bullet.swiper-pagination-bullet-active.circle-pagination::after {
  animation: circleLeft 5s linear forwards;
}

/* ==========================
  アニメーションを定義
========================== */
@keyframes circleLeft {
  0% {
    transform: rotate(0deg);
    background-color: #fff;
  }

  50% {
    transform: rotate(180deg);
    background-color: #fff;
  }

  50.01% {
    transform: rotate(360deg);
    background-color: #222;
  }

  100% {
    transform: rotate(360deg);
    background-color: #222;
  }
}

@keyframes circleRight {
  0% {
    transform: rotate(0deg);
  }

  50% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(180deg);
  }
}

script.js

const circlePaginationSlider = new Swiper('.circlePaginationSlider', {
    loop: true,
    speed: 500,
    effect: 'fade',
    autoplay: {
        delay: 5000,
        disableOnInteraction: false,
    },
    pagination: {
        el: '.swiper-pagination',
        clickable: true,
        renderBullet: function (index, className) {
            let num = index + 1;
            return (
                '<div class="' + className + ' circle-pagination' + '"><div class="circle-pagination_inner">' + num + '</div></div>'
            )
        },
    },
    on: {
        slideChange: function () {
            if (this.realIndex > 0) {
                this.params.autoplay.delay = 4500;
            }
        },
    },
});

コードの解説

今回使用したSwiperのオプションについて解説します。

paginationオプション

pagination: {
    el: '.swiper-pagination',
    clickable: true,
    renderBullet: function (index, className) {
        let num = index + 1;
        return (
            '<div class="' + className + ' circle-pagination' + '"><div class="circle-pagination_inner">' + num + '</div></div>'
        )
    },
},

renderBulletはドットのページネーションをカスタマイズする際に使用するパラメータです。ドットのHTMLを以下のように上書きしています。

ドットのHTMLの変更内容
  • ドットの要素にクラス名circle-paginationを追加し、その中にcircle-pagination_innerクラスのdivタグを追加。
  • 上記のdivタグの中にスライド番号を出力。

引数で渡ってきたindexに1を足した数字をスライド番号として出力しています。indexは0から始まるため1を加え、画面に表示される際に1から始まるようにしています。

onオプション

on: {
    slideChange: function () {
        if (this.realIndex > 0) {
            this.params.autoplay.delay = 4500;
        }
    },
},

slideChangeパラメータでスライドが遷移した際の処理を指定しています。

アクティブなスライドのインデックス番号が0より大きい場合(=スライドが2枚目以降の場合)に、スライダーの自動再生に関する設定を変更しています。

具体的には、スライドが最初のスライド以外に移動した場合に、自動再生の遅延時間を5000ミリ秒から4500ミリ秒に変更しています。

これにより、最初のスライドとそれ以外のスライドで表示時間を調整しています。

  1. スライド1枚目の自動再生の遅延時間は5000ミリ秒
  2. スライド2枚目以降の自動再生の遅延時間は4500ミリ秒

2枚目以降のスライドの遅延時間を調整しているのは、スライドアニメーションのスピードである500ミリ秒を引いているためです。

5000ミリ秒 − 500ミリ秒 = 4500ミリ秒

SVGで実装する方法

つづいてSVG画像で実装した例がこちらです。

見た目はそれほど変わりはないですが、ページネーションの背景が白色の場合、SVGで実装した方が余計な隙間が生まれずキレイに仕上がります(体験談)。

また、グラデーションの円をアニメーションさせる場合もSVGを用意した方がスムーズです。

コードの見本

index.html

<div class="wrapper">
    <div class="swiper circlePaginationSlider">
        <div class="swiper-wrapper">
            <div class="swiper-slide"><img src="./images/06.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/07.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/08.jpg" alt="サンプル画像"></div>
            <div class="swiper-slide"><img src="./images/09.jpg" alt="サンプル画像"></div>
        </div>
    </div>
    <div class="swiper-pagination"></div>
</div>

style.css

/* ==========================
  Swiperのスタイルを調整
========================== */
.wrapper {
  position: relative;
}

.swiper-slide {
  height: 500px;
}

.swiper-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* =============================
    ページネーションの見栄えを調整
============================= */
.swiper-horizontal>.swiper-pagination-bullets,
.swiper-pagination-bullets.swiper-pagination-horizontal,
.swiper-pagination-custom,
.swiper-pagination-fraction {
  width: 150px;
  background-color: #fff;
  bottom: 0;
  padding: 8px 0;
  line-height: 1;
}

.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet {
  margin: 0 3px;
}

/* ==========================
    円形のページネーションを作成
  ========================== */
.circle-pagination {
  position: relative;
  width: 25px;
  height: inherit;
  z-index: 1;
  background-color: #fff;
  text-align: center;
  cursor: pointer;
  opacity: 1;
  display: inline-block;
  outline: none;
}

.circle-pagination .circle-pagination__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #bfbfbf;
  font-size: 12px;
  font-weight: 500;
  position: relative;
}

.circle-pagination__inner svg {
  transform: rotate(-90deg);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-90deg);
}

.circle-pagination__inner circle {
  fill: transparent;
  stroke: blue;
  stroke-width: 1.5;
  animation: circle 5s linear forwards;
}

@keyframes circle {
  0% {
    stroke-dasharray: 0 69;
  }

  99.9%,
  to {
    stroke-dasharray: 69 69;
  }
}

.swiper-pagination-bullet.swiper-pagination-bullet-active .circle-pagination__inner {
  color: blue;
}

.circle-pagination:not(.swiper-pagination-bullet-active) .circle-pagination__inner svg {
  /* 非アクティブ時なサークルのsvgは非表示に */
  display: none;
}

script.js

const circlePaginationSlider = new Swiper('.circlePaginationSlider', {
    loop: true,
    speed: 500,
    effect: 'fade',
    autoplay: {
        delay: 5000,
        disableOnInteraction: false,
    },
    pagination: {
        el: '.swiper-pagination',
        clickable: true,
        renderBullet: function (index, className) {
            let num = index + 1;
            return (
                '<div class="' + className + ' circle-pagination' + '"><div class="circle-pagination__inner"><svg width="25" height="25"><circle cx="12.5" cy="12.5" r="11"></circle></svg>' + '0' + num + '</div></div>'
            )
        },
    },
    on: {
        slideChange: function () {
            if (this.realIndex > 0) {
                this.params.autoplay.delay = 4500;
            }
        },
    },
});

コードの解説

SwiperのオプションとCSSのstroke-dasharrayについて解説しています。

paginationオプション

擬似要素の場合と同じく、SwiperのpaginationオプションのrenderBulletパラメータを使い、SVG画像を出力しています。

'<div class="' + className + ' circle-pagination' + '"><div class="circle-pagination__inner"><svg width="25" height="25"><circle cx="12.5" cy="12.5" r="11"></circle></svg>' + '0' + num + '</div></div>'

stroke-dasharrayの値

CSSの@keyframesの中でstroke-dasharrayの値を変化させることでアニメーションを表現しています。

@keyframes circle {
  0% {
    stroke-dasharray: 0 69;
  }

  99.9%,
  to {
    stroke-dasharray: 69 69;
  }
}

stroke-dasharray: 破線の長さ 破線の間隔;

破線の長さの「69」は下記の計算式から導き出しています。

  • 半径11pxの円を作りたいとすると、直径は11px×2で22px。
  • 片側の線の幅(stroke-width)が1.5pxなので、両側の線の幅は1.5px×2で3px。
  • SVGの幅と高さは、22px+3px=25pxとなる。
  • 円周の長さは「直径×3.14」なので、22px×3.14=69.08。
  • 小数点以下は含めず、今回は破線の長さを69pxとした。

こちらのQiitaの記事で、「円の外周の計算方法」が詳しく解説されているのでぜひ参考にしてください。

以上です。最後までお読みいただきありがとうございました。

目次