Swiper 踩坑
Swiper 踩坑紀錄
TL;DRSwiper.js 在 React / Next.js 上的常見地雷:圖片底部空隙(display: block)、centerMode 露出半張的設定、動態更新 slides 不生效(要 observer)、SSR 相容性(動態 import)。
參考Stack Overflow — Swiper.js: how to show 2 half slides and 2 full slides
安裝
npm install swiper
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
CSS 要分別 import,別漏了 module 對應的樣式檔。
踩坑 1:圖片四周留白
圖片預設是 inline 元素,baseline 對齊會留底部空隙(放字母 'g' 的下緣空間)。
.swiper-slide img {
display: block; /* 關鍵:消除 inline baseline 空隙 */
width: 100%;
height: 100%;
object-fit: cover;
}
一勞永逸在全域 reset 加 img { display: block; },不只 Swiper,所有圖片都不會再有這問題。
踩坑 2:CenterMode 露出半張
要做「中間一張完整 + 左右各露出半張」效果:
<Swiper
slidesPerView={2.5} // ⭐ 關鍵:小數點
centeredSlides={true} // 中間對齊
spaceBetween={20}
loop={true}
>
{items.map((item) => (
<SwiperSlide key={item.id}>{item.content}</SwiperSlide>
))}
</Swiper>
slidesPerView 用 2.5 表示「顯示 2.5 張」 → 中間 1 張完整 + 左右各 0.75 張(會被 viewport 切半)。配 centeredSlides 讓 active 的那張在正中。
踩坑 3:動態更新 slides 不生效
當你的 items 是動態載入(從 API 來)、或會新增/刪除,Swiper 不會自動偵測 DOM 變化:
<Swiper
observer={true} // 監看 swiper 自身變化
observeParents={true} // 監看 parent 變化(響應式必備)
observeSlideChildren={true} // 監看 slide 內子元素變化
>
加完還是不更新?試試 useEffect + swiper.update():const swiperRef = useRef<SwiperType>(null); useEffect(() => { swiperRef.current?.update(); }, [items]); <Swiper onSwiper={(s) => (swiperRef.current = s)}>...</Swiper>
踩坑 4:Next.js SSR 相容性
Swiper 用到 window / document,在 Next.js SSR 階段會炸:
ReferenceError: window is not defined
解法 1:動態 import(推薦)
import dynamic from 'next/dynamic';
const Swiper = dynamic(
() => import('swiper/react').then((mod) => mod.Swiper),
{ ssr: false }
);
const SwiperSlide = dynamic(
() => import('swiper/react').then((mod) => mod.SwiperSlide),
{ ssr: false }
);
代價:輪播 等到 hydration 後才出現,首屏會有空白瞬間。可以加 skeleton 緩解。
解法 2:'use client' directive(App Router)
'use client';
import { Swiper, SwiperSlide } from 'swiper/react';
// ...
App Router 推薦這個,比 dynamic import 簡單,且 client component 在 streaming 模式下不會 block hydration。
踩坑 5:Pagination / Navigation 樣式覆寫
預設箭頭跟分頁點 不一定符合設計,要客製:
/* 改箭頭顏色 */
.swiper-button-next,
.swiper-button-prev {
color: var(--color-accent);
}
/* 改分頁點 */
.swiper-pagination-bullet {
background: var(--color-text-muted);
opacity: 0.5;
}
.swiper-pagination-bullet-active {
background: var(--color-accent);
opacity: 1;
}
/* 完全自訂分頁 */
.swiper-pagination-bullet-custom {
width: 30px;
height: 4px;
border-radius: 2px;
}
!important 通常不需要,Swiper 的 specificity 不算高,正常 CSS 就能覆寫。
踩坑 6:loop 模式下 onSlideChange 觸發兩次
loop: true 時,Swiper 內部用「複製首尾 slide」實現無縫循環,會讓 onSlideChange 在切換到複製品時觸發一次,真實品又觸發一次。
解法:用 realIndex 取代 activeIndex:
<Swiper
loop
onSlideChange={(swiper) => {
const realIndex = swiper.realIndex; // ⭐ 不要用 swiper.activeIndex
setCurrentSlide(realIndex);
}}
>
替代選擇
| 套件 | 大小 | 特色 |
|---|---|---|
| Swiper | ~150KB | 全功能,但較重 |
| Embla Carousel | ~10KB | 現代輕量,API 漂亮 |
| Keen Slider | ~20KB | 體積小、效能好 |
| react-slick | ~50KB | jQuery slick 移植,老牌穩定 |
2026 年建議新專案優先 Embla Carousel(現代、輕、TypeScript 完整)。Swiper 已不是首選,但如果需要它特有的功能(parallax、3D effect)還是 OK。