Splideで作るアーチ状のカルーセル、ライブラリと競合しないtransformのかけ方

中央から離れるほどスライドが下がりながら外側へ傾いていく、アーチ状(扇形)のカルーセル(スライダー)をSplideで作ってみました。

Splideの標準機能だけではこのような配置は実現できないため、JavaScriptで各スライドにtransformを加えています。その際、Splideが内部で行っているtransformによる位置制御と競合しないように工夫したので、その方法を中心に解説します。

実装例

早速ですが以下が実装例です。

アーチ状のカルーセル

スライドが中央を頂点としたアーチ状に並び、移動中もなめらかにアーチの形を保ち続けます。詳細な仕様と使用ライブラリは以下の通りです。

仕様

  • ドラッグ・スワイプに対応
  • 自動再生(ループ)
  • 5枚のスライドが見える
  • 矢印・ページネーションあり

使用ライブラリ

  • Splide 4.1.3

動作確認環境

以下の環境にて動作を確認しています。モバイルでは未確認です。

  • Chrome 149
  • Firefox 152.0.6
  • Safari 17.6

実装のポイント

工夫した点は大きく3つあります。

  • スライド直下のインナー要素にtransformをかける
  • 画面上の実座標をもとにアーチ配置を毎フレーム計算する
  • 演出用のパラメータを一元管理する

順番に見ていきます。

スライド直下のインナー要素にtransformをかける

HTMLの構造は以下のとおりです。

index.html
<div class="arch-splide splide" id="arch-carousel">
<div class="splide__track">
<ul class="splide__list">
<li class="splide__slide">
<div class="arch-slide-inner">
<div class="arch-card">01</div>
</div>
</li>
<!-- 以下繰り返し -->
</ul>
</div>
</div>

ポイントはsplide__slideの直下にarch-slide-innerというインナー要素を挟んでいる点です。アーチ演出用のtransform(回転とY方向のずらし)は、splide__slideではなくこのインナー要素に対してかけています。

Splideは内部でtransformを使ってスライドの位置を制御しています。そのためsplide__slideに対して外側からrotatetranslateを重ねると、ライブラリの制御と競合して表示が崩れてしまいます。

インナー要素を1枚挟むことで、「スライドの位置の制御はSplide」「アーチの演出は自前のJavaScript」と役割を分離でき、競合を避けられます。

担当役割
Splideスライドの位置(x軸方向)の制御
自前のJavaScriptアーチの演出(回転とY方向のずらし)

画面上の実座標をもとにアーチ配置を毎フレーム計算する

アーチ配置の計算は、スライドのindexではなく画面上の実座標をもとに行っています。以下がその処理です。

script.js
const track = root.querySelector(".splide__track");
let rafId = null;
const applyArch = () => {
const trackRect = track.getBoundingClientRect();
const centerX = trackRect.width / 2 + trackRect.left;
const half = trackRect.width / 2;
const slides = root.querySelectorAll(".splide__slide");
for (const el of slides) {
const rect = el.getBoundingClientRect();
// スライド自身の中心のX座標
const slideCenterX = rect.width / 2 + rect.left;
// スライド中心とトラック中心とのX方向の差分
const delta = slideCenterX - centerX;
// トラック半幅で正規化したx方向のオフセット: -1(左端)… 0(中央)… 1(右端)
let centerOffset = delta / half;
if (centerOffset > 1) centerOffset = 1;
if (centerOffset < -1) centerOffset = -1;
// 中心からの距離に応じた0~1の補正値(カーブを滑らかにする)
const archRate = Math.pow(Math.abs(centerOffset), EASE_POWER);
// 最終的なY方向のずらし量(px)。中心で0、端でMAX_Yに近づく
const y = MAX_Y * archRate;
// 最終的な回転角(deg)。中心からの向きに応じて端に向かって外側へ傾く
const rotate = MAX_ROTATE * centerOffset;
const inner = el.querySelector(".arch-slide-inner");
if (!inner) continue;
// delta をスライド幅で正規化し、中央(50%)を基準に transform-origin の x座標をずらす
const originX = Math.max(
0,
Math.min(100, 50 - (delta / rect.width) * ORIGIN_SHIFT),
);
inner.style.transformOrigin = `${originX.toFixed(2)}% 100%`;
inner.style.transform =
"translateY(" +
y.toFixed(2) +
"px) rotate(" +
rotate.toFixed(2) +
"deg)";
}
rafId = requestAnimationFrame(applyArch);
};

処理の流れは次のとおりです。

  1. getBoundingClientRect()でトラックの中心座標と各スライドの中心座標を取得
  2. その差分deltaをトラックの半幅で正規化し、-1(左端)〜 0(中央)〜 1(右端)のオフセット値に変換
  3. オフセット値からY方向のずらし量・回転角・transform-originを計算し、インナー要素に反映

この計算をrequestAnimationFrameで毎フレーム実行しています。

スライドのindexやカレントクラスをもとに計算する方法も考えられますが、その場合はループの境界(クローンスライドとの切り替わり)付近でかくつきが発生しがちです。実測座標ベースであればスライドが「画面上のどこにあるか」だけを見ればよいので、ループ境界・ドラッグ中・自動再生中のいずれの状態でも破綻しにくい構造です。

また、細かいですがtransform-originのx座標をORIGIN_SHIFTによって中央からずらしている点も工夫しました。回転の原点が各スライドの底辺中央のままだと扇の中心がバラバラになるため、中央から離れたスライドほど原点を内側に寄せることで、全体がひとつの弧を描いているように見せています。

なお、Splideの初期化完了後に計算を開始し、破棄時にループを止めるようにしています。

script.js
splide.on("mounted", () => {
rafId = requestAnimationFrame(applyArch);
});
splide.on("destroy", () => {
if (rafId) cancelAnimationFrame(rafId);
});
splide.mount();

演出用のパラメータを一元管理する

アーチの見た目を決める値は、コードの冒頭に定数としてまとめています。

script.js
const MAX_Y = 26; // px: 両端のスライドに適用する下方向オフセットの最大値
const MAX_ROTATE = 6.5; // deg: 両端のスライドに適用する回転角の最大値
const EASE_POWER = 2; // Y方向の減衰カーブの指数(2 = 滑らかな放物線状に変化)
const ORIGIN_SHIFT = 24; // スライド1枚分の水平オフセットあたり、transform-origin のx座標をずらす量(%pt)
パラメータ値を大きくしたときの効果
MAX_Yアーチが深くなる
MAX_ROTATEスライドの傾きが強くなる
EASE_POWER中央付近が平坦になり、端で急に下がるカーブになる
ORIGIN_SHIFT回転の原点がより内側に寄り、弧のつながりが変わる

デザインに合わせた微調整はこの4つの値を触るだけで完結するため、実案件でトーンを変えたいときにも対応しやすくなっています。なおスライドの幅はCSSで設定し、スライド間の余白はSplideのオプションで設定しています。

style.css
.splide__slide {
width: 25vw; /* スライドの幅 */
}
script.js
const splide = new Splide(root, {
gap: "16px", // スライド間の余白
// その他のオプション
});

まとめ

以上、Splideで作るアーチ状のカルーセルでした。

「Splideの制御と競合させないためにインナー要素へtransformをかける」というアプローチは、アーチ配置に限らず、スライドに独自の動きを加えたい場面全般で使えそうです。カード型の実績紹介やギャラリーなど、少しリッチな見せ方をしたいときにぜひ試してみてください。

この記事を書いた人

写真:ブール 代表 西川雅人

Masato Nishikawa

ウェブ制作会社・デザイナー様向けに、コーディングやWordPress実装を承っているフリーランスです。繁忙期のサポートや、継続的な外注先をお探しでしたら、お気軽にお声がけください。

コーディングや
WordPress実装の
ご相談を承っています。

新規制作から運用フェーズの修正まで柔軟に対応します。
お見積りや実装可否の確認など、お気軽にご連絡ください。