同一ページに複数のSwiperカルーセルを配置すると干渉する問題の解決方法
Cursorにカルーセルを作ってもらったら、最終的におかしなことに…
「ここが変だから直して」
「こっちもおかしいから直して」
「そこのコードはいじらないでっていったでしょ」
とやり取りをして直してもらったので、そのメモをCursorに書いてもらいました。
なんだったのか
- グローバルセレクタによるCSS/JSの競合が原因
- クラス名のスコープ化とユニークな識別子で解決
- グローバル変数をdata属性に置き換えて複数インスタンスに対応
環境
- Swiper.js 11.x
- HubSpot CMS(カスタムモジュール)
問題の症状
1. ナビゲーション矢印の干渉
- モジュールAの矢印をクリック → モジュールBも動く
- ページネーションドットも同様に干渉
2. センターモードの崩壊
- 単体では正常に動作
- 同一ページに複数配置すると、後から読み込まれたモジュールがレイアウトを破壊
3. グローバル変数の上書き
- autoplay設定などが複数インスタンス間で競合
原因分析
CSS セレクタの競合
/* ❌ 問題のあるコード */
.swiper-slide {
width: 480px !important;
}
.swiper-slide-active .card {
transform: scale(1.06);
}
問題点:
-
.swiper-slide
がグローバルセレクタ - 全てのSwiperインスタンスに影響
JavaScript セレクタの競合
// ❌ 問題のあるコード
navigation: {
nextEl: ".swiper-button-next", // グローバルセレクタ
prevEl: ".swiper-button-prev",
}
問題点:
-
querySelector
が最初にマッチした要素のみを返す - 複数カルーセルで同じ要素が操作対象になる
グローバル変数の競合
// ❌ 問題のあるコード
<script>
var autoplayEnabled = {{ module.carousel.autoplay|lower }};
var autoplayDelay = {{ module.carousel.autoplay_ms or 4000 }};
</script>
問題点:
- 後から読み込まれたモジュールが変数を上書き
- 全インスタンスが最後の設定値を参照
解決策
1. CSS セレクタのスコープ化
/* ✅ 修正後 */
.lpSwiper .swiper-slide {
width: 480px !important;
}
.lpSwiper .swiper-slide-active .lp-card {
transform: scale(1.06);
}
.hubdbSwiper .swiper-slide {
width: 480px !important;
}
.hubdbSwiper .swiper-slide-active .hubdb-card {
transform: scale(1.06);
}
ポイント:
- モジュール固有のルートクラスでスコープ化
- メディアクエリ内も全てスコープ化が必要
2. ユニークなナビゲーションセレクタ
HTML:
<!-- LP Carousel -->
<div class="swiper lpSwiper">...</div>
<div class="lpSwiper-controls">
<div class="swiper-button-prev lp-button-prev"></div>
<div class="lp-swiper-pagination"></div>
<div class="swiper-button-next lp-button-next"></div>
</div>
<!-- HubDB Carousel -->
<div class="swiper hubdbSwiper">...</div>
<div class="hubdbSwiper-controls">
<div class="swiper-button-prev hubdb-button-prev"></div>
<div class="hubdb-swiper-pagination"></div>
<div class="swiper-button-next hubdb-button-next"></div>
</div>
JavaScript:
// ✅ 修正後
allCarousels.forEach(function(rootEl) {
// コントロール要素を兄弟要素から探す
var controlsEl = rootEl.nextElementSibling;
while (controlsEl && !controlsEl.classList.contains('lpSwiper-controls')) {
controlsEl = controlsEl.nextElementSibling;
}
var swiperConfig = {
// ...
pagination: {
el: controlsEl ? controlsEl.querySelector('.lp-swiper-pagination') : null,
},
navigation: {
nextEl: controlsEl ? controlsEl.querySelector('.lp-button-next') : null,
prevEl: controlsEl ? controlsEl.querySelector('.lp-button-prev') : null,
},
};
new Swiper(rootEl, swiperConfig);
});
ポイント:
- 各カルーセルインスタンスごとに個別のコントロール要素を探索
-
querySelector
のスコープを制限
3. グローバル変数をdata属性に置き換え
HTML:
<!-- ✅ 修正後 -->
<div class="swiper lpSwiper"
data-autoplay="{{ module.carousel.autoplay|lower }}"
data-autoplay-delay="{{ module.carousel.autoplay_ms or 4000 }}">
<!-- ... -->
</div>
JavaScript:
// ✅ 修正後
allCarousels.forEach(function(rootEl) {
// data属性から設定を読み取る
var autoplayEnabled = rootEl.getAttribute('data-autoplay') === 'true';
var autoplayDelay = parseInt(rootEl.getAttribute('data-autoplay-delay'), 10) || 4000;
var swiperConfig = {
// ...
};
if (autoplayEnabled) {
swiperConfig.autoplay = {
delay: autoplayDelay,
disableOnInteraction: false,
pauseOnMouseEnter: true
};
}
new Swiper(rootEl, swiperConfig);
});
ポイント:
- インスタンスごとに独立した設定を保持
- グローバルスコープを汚さない
4. センターモードの確立
/* overflow設定の調整 */
.lpSwiper {
overflow: hidden; /* visibleからhiddenに変更 */
padding-bottom: 40px; /* 拡大時の影が切れないように */
margin-bottom: -40px; /* 余白を相殺 */
}
.lpSwiper .swiper-wrapper {
display: flex;
align-items: center;
}
// 初期化時に明示的にスライド位置を設定
on: {
init: function (sw) {
sw.update();
if (slideCount > 1) {
sw.slideToLoop(0, 0, false); // 最初のスライドに移動
}
// ...
}
}
結果
✅ 複数のSwiperインスタンスが完全に独立して動作
✅ ナビゲーションの干渉なし
✅ センターモードが正常に機能
✅ 各インスタンスが個別の設定を保持
まとめ
複数のSwiperカルーセルを同一ページに配置する際は:
- CSS/JSセレクタを必ずスコープ化(モジュール固有のクラスで囲む)
- ナビゲーション要素にユニークな識別子を付与
- グローバル変数を避け、data属性やクロージャで管理
- DOM探索はインスタンスごとにスコープを制限
これらを徹底することで、カルーセルの干渉問題を完全に解決できます。