完成イメージ
手順
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;
}