CSSだけで移動ボタン付きカルーセルが作れる時代がくるかもしれない

2025年 8月 8日 Posted 野々瀨(フロントエンドエンジニア)

スクロールによってコンテンツを吸着させるscroll-snap-typeプロパティといったプロパティを使用することで、CSSのみで簡単なカルーセルを作ることができます。しかし、この段階ではまだまだカルーセルとしてはJavaScriptでの実装が多くあります。

最近、前後の移動が可能なナビゲーションボタンやページネーションの実装も、CSSで可能になるらしいと話題になっていましたので、筆者も試してみました。

事前準備

まず最低限として水平方向のカルーセルで、スクロール時に吸着する実装をします。次のようなHTMLを用意します。

<div class="carousel">
    <div class="item">Item 1</div>
    <div class="item">Item 2</div>
    <div class="item">Item 3</div>
    <div class="item">Item 4</div>
    <div class="item">Item 5</div>
</div>

続いて次のようなCSSを用意します。

.carousel {
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    display: flex;

    .item {
        scroll-snap-align: start;
        scroll-snap-stop: always;
        display: flex;
        justify-content: center;
        align-items: center;
        flex: 1 0 auto;
        width: 320px;
        aspect-ratio: 16 / 9;

        &:nth-child(1) {
            background-color: #e9bbbb;
        }

        &:nth-child(2) {
            background-color: #e9e2bb;
        }

        &:nth-child(3) {
            background-color: #c3e9bb;
        }

        &:nth-child(4) {
            background-color: #bbe0e9;
        }

        &:nth-child(5) {
            background-color: #e5bbe9;
        }
    }
}

ポイントとしては次のような感じです。

ページネーションの実装

※ 準備段階でのHTMLとCSSはそのまま生かします。

次のようにCSSを追記します。

.carousel {
    scroll-marker-group: after;

    &::scroll-marker-group {
        display: flex;
        justify-content: center;
        gap: 20px;
        margin-top: 10px;
        height: 16px;
    }

    .item {
        &::scroll-marker {
            border-radius: 50%;
            aspect-ratio: 1 / 1;
            background-color: #666;
            content: "";
        }

        &::scroll-marker:target-current {
            background-color: #ff008c;
        }
    }
}

.carousel要素に対してscroll-marker-groupプロパティを指定します。scroll-marker-groupプロパティは、::scroll-marker-group疑似要素を出力するために指定します。値はどこへ出力するかを指定しますが、ここではafterを指定して.carousel要素の次の要素として出力するようにします。

::scroll-marker-group疑似要素は、ページネーションの本体(親)要素を出力します。

::scroll-marker疑似要素は、ページネーションのアイテム(子)要素を出力します。

:target-current疑似クラスは、現在のスクロール位置となる::scroll-marker疑似要素を指します。

なお、2025年5月現在はボタンを押した際のスクロールにアニメーションは付きません。

番号の場合

番号の場合は、次のようにcounter-incrementプロパティとcounter関数で番号として付けることができます。

.carousel {
    scroll-marker-group: after;

    &::scroll-marker-group {
        display: flex;
        justify-content: center;
        gap: 20px;
        margin-top: 10px;
        height: 1em;
    }

    .item {
        counter-increment: markers;

        &::scroll-marker {
            content: counter(markers);
            color: #666;
            text-decoration: none;
        }

        &::scroll-marker:target-current {
            color: #ff008c;
            font-weight: bold;
        }
    }
}

ナビゲーションボタンの実装

※ 準備段階でのHTMLとCSSはそのまま生かします。

次のようにCSSを追記します。

.carousel {
    anchor-name: --carousel;

    &::scroll-button(*) {
        position: absolute;
        position-anchor: --carousel;
        align-self: anchor-center;
        margin: 10px 0 0;
        padding: 0;
        border: none;
        width: 20px;
        height: 26px;
        background-color: #333;
    }

    &::scroll-button(left) {
        left: calc(anchor(left) + 10px);
        clip-path: polygon(100% 0, 100% 100%, 0 50%);
        content: "";
    }

    &::scroll-button(right) {
        right: calc(anchor(right) + 10px);
        clip-path: polygon(0 0, 100% 50%, 0 100%);
        content: "";
    }

    &::scroll-button(*):where(:hover, :focus-visible) {
        background-color: #555;
    }

    &::scroll-button(*):enabled {
        cursor: pointer;
    }

    &::scroll-button(*):disabled {
        background-color: #eee;
        cursor: default;
    }
}

.carousel要素に対して::scroll-button疑似要素を指定します。::scroll-button疑似要素は、上下左右の各ボタン要素を出力します。引数には各ボタンの対象を指定します。*は左右、leftは水平軸の左、rightは水平軸の右として指定しています。

:enabled疑似クラスで、有効状態のスタイルを指定します。

:disabled疑似クラスで、無効状態のスタイルを指定します。

またカルーセルの上に中央揃えでのせるため、アンカーポジショニングを用いています。anchor-nameプロパティを基準となるセレクターに指定し、値はアンカー名称を付けます。position-anchorプロパティで対象となるアンカー名称を指定します。align-selfプロパティを値としてanchor-centerを指定して上下中央揃えにします。左右のボタンに対してanchor関数を指定し、引数は左端としてleft、右端としてrightを指定しています。

なお、2025年5月現在はボタンを押した際のスクロールにアニメーションは付きません。

対応ブラウザー

2025年5月現在では、EdgeとChromeのバージョン135以上でのみ対応しています。

https://caniuse.com/mdn-css_properties_scroll-marker-group

https://caniuse.com/?search=scroll-button

【余談】ドラッグやスワイプのスクロールは……

説明の中にある画像でお見せした部分は、全てモバイルのタッチ操作による動作デモです。現状ドラッグやスワイプのスクロールを実装するには、JavaScriptの手を借りる必要があります。しかし残念ながら、現状はscroll-snap-typeプロパティとマウス系のイベントを一緒に動作させることはできません。

【余談】スクロールに関するイベント

スクロールに関して次のようなイベントがあります。

scrollイベント

scrollイベントは、これまでずっとあるイベントですが、スクロールしている時に発生するイベントです。

document.querySelector('.carousel').addEventListener('scroll', () => {
    console.log('scrolling');
});

scrollendイベント

scrollendイベントは、スクロールを終えた時に発生するイベントです。

document.querySelector('.carousel').addEventListener('scrollend', () => {
    console.log('scrollend');
});

2025年5月現在では、Safari以外のブラウザーで対応しています。

scrollsnapchangingイベント

scrollsnapchangingイベントは、スクロール中に吸着する要素に達した時に発生するイベントです。scroll-snap-alignプロパティにnone以外を指定している時に発生します。引数からEvent.snapTargetInlineプロパティで吸着する要素を取得することができます。

document.querySelector('.carousel').addEventListener('scrollsnapchanging', event => {
    console.log('changing', event.snapTargetInline);
});

2025年5月現在では、EdgeとChromeのバージョン129以上でのみ対応しています。

scrollsnapchangeイベント

scrollsnapchangeイベントは、スクロールが終了した時に吸着する要素に達している時に発生します。引数からEvent.snapTargetInlineプロパティで吸着する要素を取得することができます。

document.querySelector('.carousel').addEventListener('scrollsnapchange', event => {
    console.log('change', event.snapTargetInline);
});

2025年5月現在では、EdgeとChromeのバージョン129以上でのみ対応しています。

参考リンク

最後に

実際に案件で実装するにはまだ少し先の話となるでしょうが、ナビゲーションやページネーション付きのカルーセルをCSSで簡単に実装できる時代が近づいてきています。もちろん高機能なカルーセルを実装する場合はやはりJavaScriptは必要でしょう。HTMLとCSSだけで簡単になんでも実装できるようになってきていますね。