home / blog / snap-scrollとタブ切り替えが出来るカルーセルを作成する

snap-scrollとタブ切り替えが出来るカルーセルを作成する

実行結果

cssのsnap-scrollと、タブ切り替えを同時に使えるように実装しました。

スワイプでのスクロールと、タブタイトルのタップでのスクロールの両方できるかと思います。

コード解説 ※一部抜粋なのでコピペは上のソースコードからして下さい。

HTML

<!-- タブ -->
<div id="tab">
    <input type="radio" name="tab-radio" checked>
    <label class="tab-label">A</label>

    <input type="radio" name="tab-radio">
    <label class="tab-label">B</label>

    <input type="radio" name="tab-radio">
    <label class="tab-label">C</label>
    
    <!-- アンダーバー -->
    <div id="select-tab-cover"></div>
</div>

ラジオボタンでアクティブなアイテムを管理します。

ラジオボタンは隠すのでlabelをクリックしたときにスクロールを発火させます。

アクティブなアイテムにアンダーバーを表示するため、タブ内にdivを一つ追加しています。

<!-- カルーセル -->
<div id="carousel">
    <div class="carousel-item" id="item-a">アイテムA</div>
    <div class="carousel-item" id="item-b">アイテムB</div>
    <div class="carousel-item" id="item-c">アイテムC</div>
</div>

カルーセルはシンプルです。

CSS

.tab-label {
    opacity: 0.2;
    transition: all 1s ease; /* 不透明度が1秒かけて変化する *
}

input[name="tab-radio"]:checked+.tab-label {
    opacity: 1; /* アクティブタブの不透明度を1にする */
}

ラジオボタンがチェックされるとラベルの不透明度が1に変化します。

/* カルーセル */
#carousel {
    display: flex;
    overflow-x: auto;
    width: 100%;
    height: 300px;
    scroll-snap-type: x mandatory; /* スクロールをピッタリ止める */
}

.carousel-item {
    scroll-snap-align: center; /* ページの真ん中でスクロールを止める */
    scroll-snap-stop: always; /* スクロール時にページを飛ばさない。 */
    width: 100%;
    height: 100%;
    flex-shrink: 0;
}

scroll-snap-type: x mandatory; を指定してスクロールがカルーセルアイテム毎にピッタリ止まるようにします。

scroll-snap-align: center; を指定してスクロールをカルーセルアイテムの真ん中で止まるようにします。

scroll-snap-stop: always; を指定して一つ隣のカルーセルアイテムで必ずスクロールが止まるようにします。

javascript

// ラベルクリック時にスライドアニメーションを実行
for (let i = 0; i < tabNum; i++) {
    tabLabel[i].onclick = () => {
        tabSlideAnim(i);
    }
}

ラベルのonclickイベント発生時にスライドアニメーションを実行します。

// スライドアニメーション
const tabSlideAnim = (x) => {
    const loopNum = 32; // アニメーション分割数 (値が大きいほどスライドが速くなる)
    let count = 0;
    const startScroll = carousel.scrollLeft;
    const diffScroll = (carousel.offsetWidth * x - startScroll);
    carousel.style.scrollSnapType = 'none'; // スクロールを一時的に無効化
    carousel.style.overflowX = 'hidden'; // スクロールを一時的に無効化
    cancelAnimationFrame(loopHandler); // 前に実行されていたアニメーションをキャンセル
    const loop = () => {
        if (count < loopNum) {
            count++;
            carousel.scrollLeft = startScroll + easeOut(count / loopNum) * diffScroll;
            loopHandler = requestAnimationFrame(loop); // loopを再帰的に呼び出す
        } else {
            carousel.style.scrollSnapType = 'x mandatory'; // スクロールを有効に戻す
            carousel.style.overflowX = 'auto'; // スクロールを有効に戻す
            carousel.scrollLeft = carousel.offsetWidth * x;
        }
    }
    loop();
}

loopNumに何回のフレーム更新でスライドさせるか指定

countをアニメーション毎に+1していき、スライドの進行度を記録

startScrollに最初のスクロール位置を保存

diffScrollに目標スクロール位置との差分を保存

carousel.style.scrollSnapType = ‘none’; carousel.style.overflowX = ‘hidden’; でスライドアニメーション中にスクロールが競合しないようにする。

cancelAnimationFrame(loopHandler) で元々進行していたアニメーションをキャンセルします。

carousel.scrollLeft = startScroll + easeOut(count / loopNum) * diffScroll; でスクロール位置を更新。

easeOut(count / loopNum)は後述。

loopHandler = requestAnimationFrame(loop); でloop関数を再帰的に呼び出すことで繰り返しを実行する。loopHandlerにハンドラーを保存しておき、アニメーションをキャンセルできるようにする。

countがloopNumに達した時、スクロールを有効に戻し、スクロール位置を改めて指定する。

// スライドを滑らかに止める用の計算
const easeOut = (p) => {
    return p * (2-p);
}

スクロールにイージングをかけるための関数を定義します。

carousel.onscroll = (e) => {
    // アンダーバーを連動して動かす。
    coverX = 100 * e.target.scrollLeft / carousel.offsetWidth;
    cover.style.transform = 'translateX(' + coverX + '%)';

    // スクロール量を取得してラジオボタンに反映させる
    if (e.target.scrollLeft < carousel.offsetWidth / 2) {
        tabRadio[0].checked = true;
    } else
    if (e.target.scrollLeft < carousel.offsetWidth * 3 / 2) {
        tabRadio[1].checked = true;
    } else {
        tabRadio[2].checked = true;
    }
}

アクティブなアイテムに表示するアンダーバーをスクロールに連動させます。

スクロール位置が二つのアイテムの真ん中でラジオボタンのチェックを切り替えます。

以上