スクロール操作に合わせて要素がピタッと所定位置に止まる「スナップ」挙動は、CSSのscroll-snapで実装できます。JavaScriptなしでも横スクロールのカードスライダーや縦セクションのフルスクリーン風ページを再現でき、操作感が安定するためモバイルでも快適に使えます。ここでは基本構文から実用的なスライダーUIのコード、ヘッダー固定時の調整やアクセシビリティの勘所までをまとめます。
基本構文と考え方
親コンテナにscroll-snap-typeを指定し、子要素にscroll-snap-alignで整列位置を与えます。軸はxまたはy、強度はmandatory(必ず止まる)かproximity(近いときだけ止まる)を選びます。子要素にはstart・center・endのどこで揃えるかを指定し、必要に応じてsnap-stopで連続スワイプ時の取りこぼしを防ぎます。
横方向カードスライダーの最小実装
<section class="slider" aria-label="おすすめ記事">
<article class="card">Card 1</article>
<article class="card">Card 2</article>
<article class="card">Card 3</article>
<article class="card">Card 4</article>
</section>
.slider {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 80%;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth; /* 矢印ボタン等と併用で滑らかに */
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain; /* 親スクロールへ波及させない */
padding: 8px;
}
.card {
background: #f6f7f9;
border-radius: 12px;
padding: 24px;
scroll-snap-align: start; /* 各カードの先頭で止める */
scroll-snap-stop: always; /* 速いフリックでも取りこぼさない(対応ブラウザ) */
}
カード幅はgrid-auto-columnsで割合指定にしておくと、表示幅に応じて1.0~1.2枚程度が見切れるスライダーになります。scroll-snap-type: x mandatoryで横方向に必ずスナップし、子要素のscroll-snap-align: startでカード左端に揃えます。
矢印ボタンでの操作を追加する
CSSだけでもスナップしますが、操作補助としてスクロール量を固定して動かすと使いやすくなります。scrollByとscroll-behavior: smoothの組み合わせで実装します。
<div class="wrap">
<button class="nav prev" aria-label="前へ">‹</button>
<section class="slider" id="slider1">...同上...</section>
<button class="nav next" aria-label="次へ">›</button>
</div>
.wrap { position: relative; }
.nav {
position: absolute; top: 50%; transform: translateY(-50%);
inline-size: 40px; block-size: 40px; border-radius: 50%;
border: none; background: rgba(0,0,0,.55); color: #fff; cursor: pointer;
}
.nav.prev { left: 4px; } .nav.next { right: 4px; }
<script>
const slider = document.getElementById('slider1');
const step = () => slider.clientWidth * 0.8; // カード幅に合わせて前進
document.querySelector('.nav.next').addEventListener('click', () => {
slider.scrollBy({ left: step(), behavior: 'smooth' });
});
document.querySelector('.nav.prev').addEventListener('click', () => {
slider.scrollBy({ left: -step(), behavior: 'smooth' });
});
</script>
スナップはCSSが担うため、JavaScript側は「移動するだけ」で十分です。ユーザーの手動スクロールとも自然に共存します。
固定ヘッダーと併用する縦スナップの調整
固定ヘッダーがあるページで縦セクションをスナップする場合、scroll-paddingでオフセットを確保します。これによりヘッダーに隠れずに見出し位置で止まります。
header.sticky { position: sticky; top: 0; block-size: 64px; }
main {
height: 100dvh; overflow-y: auto;
scroll-snap-type: y proximity;
scroll-padding-top: 64px; /* 固定ヘッダー分の余白を確保 */
}
.section {
min-block-size: 100dvh;
scroll-snap-align: start;
display: grid; place-items: center;
}
proximityにしておくと通常スクロールは自然で、停止位置付近でだけ吸着します。強制的に止めたい場合はmandatoryを選びます。
入れ子スナップと慣性の衝突を避ける設定
横スナップのカード群を縦スクロールの中に入れると、軸の衝突で意図しないスクロールが起こることがあります。overscroll-behaviorで親への慣性伝播を抑制し、touch-actionで水平方向のジェスチャーを許可すると安定します。
.slider {
overscroll-behavior-inline: contain; /* 横方向の慣性を内側に閉じる */
touch-action: pan-x; /* 横パンを許可、縦は親に委ねる */
}
スクロールバー表示とアクセシビリティの配慮
見た目のスライダーでも実態は通常のスクロールコンテンツです。キーボード操作ではTabでフォーカス移動、Shift+スクロールで横移動が可能です。視覚的にスクロールが可能と分かるよう、カードの端を少し見切らせたり、:focus-visibleでフォーカスリングを整えると迷いが減ります。読み上げユーザーにはaria-labelやaria-roledescriptionで「カルーセル」風の説明を添えると親切です。
よくあるつまずきと対処
スナップしない場合は親にoverflowが設定されているか、スクロール軸とscroll-snap-typeの軸が一致しているか、子にscroll-snap-alignが付いているかを確認します。Safariの旧実装ではscroll-snap-stop未対応や細かな差異が残るため、取りこぼしが気になる場合はmandatoryとカード余白の見直しで体感を補正します。矢印ボタンでの移動量は子要素幅に合わせて計算し、端での余白を考慮すると端詰まりを回避できます。
まとめ
scroll-snapは「親に軸と強度、子に整列位置」を与えるだけでスライダー風の操作感を実現できます。横カードスライダーや縦セクションの区切り、固定ヘッダー併用時のscroll-padding、入れ子時のoverscroll-behaviorとtouch-actionまで押さえれば、モバイルでも快適なスナップUIが完成します。まずはmandatory×startの基本形から導入し、必要に応じてproximityやsnap-stop、補助ボタンを組み合わせて体験を磨いていきましょう。