【Next.js TypeScript 】React Swiper カルーセルスライダー実装、スライドが中央寄せにする方法

完成イメージ

手順

Next.js プロジェクトを用意します

npx create-next-app@latest my-carousel-app

プロジェクトに必要な依存関係をインストール

Swiperと必要なアイコンライブラリをインストールします

# Swiperのインストール
npm install swiper

# Font Awesome(ナビゲーション用アイコン)のインストール
npm install @fortawesome/react-fontawesome @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons

# 型定義のインストール(TypeScriptを使用する場合)
npm install --save-dev @types/react-fontawesome

コンポーネントの実装

my-nextjs-app/
├── src/
│   ├── app/
│   │   └── page.tsx          # メインページ
│   ├── components/
│   │   └── ReactSwiper.tsx   # カルーセルコンポーネント
│   └── styles/
│       └── reactSwiper.module.css  # コンポーネント用スタイル
├── package.json
└── ...その他の設定ファイル
ReactSwiper.tsx # カルーセルコンポーネント
"use client";
import React from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay, Mousewheel } from "swiper/modules";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faChevronLeft,
  faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import styles from "@/styles/reactSwiper.module.css";

interface CarouselProps {
  images?: string[];
}

const ReactSwiper: React.FC<CarouselProps> = ({ images }) => { 
  // 画像がない場合のデフォルト画像
  const defaultImages = [
    "https://bizlabo.site/assets/sample-img/column1.jpg",
    "https://bizlabo.site/assets/sample-img/column2.jpg",
    "https://bizlabo.site/assets/sample-img/column3.jpg",
    "https://bizlabo.site/assets/sample-img/column4.jpg",
    "https://bizlabo.site/assets/sample-img/column5.jpg",
    "https://bizlabo.site/assets/sample-img/column6.jpg",
    "https://bizlabo.site/assets/sample-img/column7.jpg",
    "https://bizlabo.site/assets/sample-img/column8.jpg",
  ];

  const imagesToUse = images || defaultImages;

  return (
    <div className={styles.container}>
      <Swiper
        modules={[
          Navigation, // ナビゲーション(次へ/前へボタン)を有効にするモジュール
          Pagination, // ページネーション(スライドのインジケーター)を有効にするモジュール
          Autoplay,   // 自動再生を有効にするモジュール
          Mousewheel  // マウスホイールでスライドをスクロールできるようにするモジュール
        ]}        spaceBetween={10} // スライド間のスペースを30pxに設定
        navigation={{
          nextEl: `.${styles.swiperButtonNext}`, // 次へボタン
          prevEl: `.${styles.swiperButtonPrev}`, // 前へボタン
        }}
        pagination={{
          clickable: true, // ページネーションをクリック可能に
          bulletClass: styles.bullet,
          bulletActiveClass: styles.bulletActive,
        }}
        // autoplay={{ delay: 3000 }}
        loop
        centeredSlides // スライドを中央に表示
        slidesPerView={1}
        breakpoints={{
          0: {
            slidesPerView: 1.4,
          },
          768: {
            slidesPerView: 2.2,
          },
          1024: {
            slidesPerView: 4,
          },
        }}
        speed={800}
        className={styles.reactSwiper} // Swiperコンポーネントにクラス名を追加
      >
        {imagesToUse.map((image, index) => (
          <SwiperSlide key={index} className={styles.slide}>
            <img
              src={image}
              alt={`Slide ${index}`}
              className={styles.slideImage}
            />
          </SwiperSlide>
        ))}
        <div className={styles.swiperButtonPrev}>
          <FontAwesomeIcon icon={faChevronLeft} />
        </div>
        <div className={styles.swiperButtonNext}>
          <FontAwesomeIcon icon={faChevronRight} />
        </div>
      </Swiper>
    </div>
  );
};

export default ReactSwiper;
reactSwiper.module.css # コンポーネント用スタイル
.container {
  position: relative;
  padding: 2rem 0;
  overflow: hidden;
}

.slide {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 90%;
}

.slideImage {
  width: 100%;
  height: auto;
  cursor: pointer;
}

.swiperButtonPrev,
.swiperButtonNext {
  position: absolute;
  top: calc(50% - 20px);
  transform: translateY(-50%);
  z-index: 2;
  cursor: pointer;
  color: #000;
  font-size: 4vw;
  transition: color 0.3s ease;
  background-color: rgba(255, 255, 255, 0.8);
  border-radius: 50%;
  width: 6vw;
  height: 6vw;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: all 0.3s ease;
}

.swiperButtonPrev:hover,
.swiperButtonNext:hover {
  color: #666;
  background-color: rgba(255, 255, 255, 0.6);
  width: 7vw;
  height: 7vw;
}

.swiperButtonPrev {
  left: 10px;
}

.swiperButtonNext {
  right: 10px;
}

.bullet {
  width: 12px;
  height: 12px;
  display: inline-block;
  border-radius: 50%;
  background: #ccc;
  margin: 0 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.bullet:hover {
  background: #666;
  width: 14px;
  height: 14px;
}

.bulletActive {
  background: #000;
}

.reactSwiper {
  padding-bottom: 40px;
}

/* .reactSwiper :global(.swiper-slide) {
  opacity: 1;
} */

/* .reactSwiper :global(.swiper-slide:not(.swiper-slide-active)) {
  opacity: 0.2;
} */

.reactSwiper :global(.swiper-pagination) {
  bottom: 0;
}

スライドの中に画像だけでなくテキストも入れる場合

reactSwiperVoice.tsx # コンポーネント
'use client'
import React, { ReactNode } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Navigation, Pagination, Autoplay } from 'swiper/modules'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
    faChevronLeft,
    faChevronRight,
} from '@fortawesome/free-solid-svg-icons'
import 'swiper/css'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
import styles from '@/styles/reactSwiperVoice.module.css'

interface SlideItem {
    id: number
    customerSrc: string
    customerAlt: string
    text: ReactNode
}

const ReactSwiperVoice: React.FC = () => {
    const slideItems: SlideItem[] = [
        {
            id: 1,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-1.png',
            customerAlt: 'Customer01',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 2,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-2.png',
            customerAlt: 'Customer02',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 3,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-3.png',
            customerAlt: 'Customer03',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 4,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-4.png',
            customerAlt: 'Customer04',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 5,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-5.png',
            customerAlt: 'Customer05',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                </>
            ),
        },
        {
            id: 6,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-6.png',
            customerAlt: 'Customer06',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト
                        
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    </span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 7,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-7.png',
            customerAlt: 'Customer07',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
        {
            id: 8,
            customerSrc: 'https://bizlabo.site/assets/staff-img/staff-8.png',
            customerAlt: 'Customer08',
            text: (
                <>
                    テキスト
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                    <br />
                    <br />
                    <span className={styles.marker}>テキストテキストテキストテキスト</span>
                    <br />
                    テキストテキストテキストテキストテキストテキストテキストテキスト
                </>
            ),
        },
    ]

    return (
        <div className={styles.container}>
            <h2 className={styles.title}>お客様の声</h2>
            <Swiper
                modules={[Navigation, Pagination, Autoplay]}
                spaceBetween={1} // スライド間のスペース
                navigation={{
                    nextEl: `.${styles.swiperButtonNext}`, // 次へボタン
                    prevEl: `.${styles.swiperButtonPrev}`, // 前へボタン
                }}
                pagination={{
                    clickable: true, // ページネーションをクリック可能に
                    bulletClass: styles.bullet,
                    bulletActiveClass: styles.bulletActive,
                }}
                autoplay={{ delay: 2000 }} // 自動再生
                loop
                centeredSlides // スライドを中央に表示
                // 1画面に表示するスライド数
                breakpoints={{
                    0: {
                        slidesPerView: 1.2,
                    },
                    768: {
                        slidesPerView: 2.4,
                    },
                    1024: {
                        slidesPerView: 4,
                    },
                }}
                speed={800} // スライドアニメーションのスピード
                className={styles.reactSwiper} // Swiperコンポーネントにクラス名を追加

                slideActiveClass={styles.slideActive} // アクティブなスライドにクラス名を追加
                slidePrevClass={styles.slidePrev} // 前のスライドにクラス名を追加
                slideNextClass={styles.slideNext} // 次のスライドにクラス名を追加
            >
                {slideItems.map((slideitem, index) => (
                    <SwiperSlide key={index} className={styles.slide}>
                        <div className={styles.slideHeader}>
                            <img
                                src={slideitem.customerSrc}
                                alt={slideitem.customerAlt}
                                className={styles.slideCustomer}
                            />
                        </div>
                        <p className={styles.slideText}>{slideitem.text}</p>
                    </SwiperSlide>
                ))}
                <div className={styles.swiperButtonPrev}>
                    <FontAwesomeIcon icon={faChevronLeft} />
                </div>
                <div className={styles.swiperButtonNext}>
                    <FontAwesomeIcon icon={faChevronRight} />
                </div>
            </Swiper>
        </div>
    )
}

export default ReactSwiperVoice
reactSwiperVoice.module.css # コンポーネント用スタイル
.container {
    position: relative;
    padding: 2rem 0;
    overflow: hidden;
}

.title {
    font-size: 1.5rem;
    font-weight: bold;
    margin-bottom: 2rem;
    text-align: center;
}

.swiper-wrapper {
    align-items: stretch;
}

.slide {
    border: solid 6px #555;
    border-radius: 0.5rem;
    display: flex;
    flex-direction: column;
    height: auto; /* 高さをそろえるため */
    box-sizing: border-box;
    transition: all 0.3s ease;
    transform: scale(0.9)!important;
}

.slideActive {
    transform: scale(1)!important;
}



.slideHeader {
    background-color: #ececec;
    text-align: center;
}

.slideCustomer {
    width: 40%;
    height: auto;
    object-fit: contain;
    vertical-align: bottom;
}

.slideText {
    padding: 1rem;
    font-size: clamp(0.75rem, 1.2vw, 1.5rem);
}

.marker {
    background-size: 0 0;
    background-position: left bottom;
    background-repeat: no-repeat;
    background-image: linear-gradient(to right, #ffb180, #ffb180);
    transition: background-size 0.8s ease;
}

.slidePrev .marker,
.slideNext .marker {
    background-size: 0 0;
}

.slideActive .marker {
    background-size: 100% 0.5rem;
}

.swiperButtonPrev,
.swiperButtonNext {
    position: absolute;
    top: calc(50% - 20px);
    transform: translateY(-50%);
    z-index: 2;
    cursor: pointer;
    color: #fff;
    font-size: 4vw;
    transition: color 0.3s ease;
    background-color: rgba(0, 0, 0, 0.8);
    border-radius: 50%;
    width: 6vw;
    height: 6vw;
    display: flex;
    justify-content: center;
    align-items: center;
    transition: all 0.3s ease;
}

.swiperButtonPrev:hover,
.swiperButtonNext:hover {
    background-color: rgba(0, 0, 0, 0.6);
}

.swiperButtonPrev {
    left: 10px;
}

.swiperButtonNext {
    right: 10px;
}

.bullet {
    width: 12px;
    height: 12px;
    display: inline-block;
    border-radius: 50%;
    background: #ccc;
    margin: 0 4px;
    cursor: pointer;
    transition: all 0.3s ease;
}

.bullet:hover {
    background: #666;
    width: 14px;
    height: 14px;
}

.bulletActive {
    background: #000;
}

.reactSwiper {
    padding-bottom: 40px;
}

.reactSwiper :global(.swiper-pagination) {
    bottom: 0;
}

アクティブなスライドを中央寄せにならないときの解決方法

(問題)アクティブなスライドを中央に表示させるにはcenteredSlides: trueでなるはずですが、ずれてしまう

box-sizing: border-box;に指定

スライドに聞いているプロパティが何かしら影響していることがあるので要注意です

.slide {
    border: solid 6px #555;
    border-radius: 0.5rem;
    display: flex;
    flex-direction: column;
    height: auto; /
    box-sizing: border-box;* 中央寄せに */
    transition: all 0.3s ease;
    transform: scale(0.9)!important;
}

参考サイト