CSS の scroll-snap で気持ちいいスクロール体験をつくる

スマホアプリやカルーセル UI でよく見かける「ピタッと止まるスクロール」は、CSS の scroll-snap プロパティだけで実現できます。JavaScript によるスクロール制御が不要になるため、実装がシンプルになり、パフォーマンスも向上します。

scroll-snap の基本的な仕組み

scroll-snap は、スクロールコンテナとその子要素の 2 つに対してプロパティを設定することで機能します。コンテナ側で「スナップの方向と挙動」を指定し、子要素側で「どこにスナップするか」を指定するという役割分担になっています。

コンテナ側(scroll-snap-type)

スクロール方向(x / y)とスナップの強制度(mandatory / proximity)を指定する

子要素側(scroll-snap-align)

各要素がスナップポイントのどこに揃うか(start / center / end)を指定する

もっともシンプルな横スクロール

まずは横方向にカードが並び、1 枚ずつピタッと止まるレイアウトを作ってみましょう。

.container {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 16px;
}

.card {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

scroll-snap-type の第 1 引数 x はスナップ方向を示し、第 2 引数 mandatory はスクロールが止まるとき必ず最寄りのスナップポイントに吸着することを意味します。子要素の scroll-snap-align: start は、各カードの左端がコンテナの左端に揃う位置でスナップすることを表しています。

HTML
CSS
JavaScript
<div class="snap-container">
  <div class="snap-card" style="background:#6366f1">Card 1</div>
  <div class="snap-card" style="background:#8b5cf6">Card 2</div>
  <div class="snap-card" style="background:#a855f7">Card 3</div>
  <div class="snap-card" style="background:#d946ef">Card 4</div>
</div>
.snap-container {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 12px;
  border-radius: 8px;
}
.snap-card {
  flex: 0 0 100%;
  scroll-snap-align: start;
  height: 120px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 18px;
  font-weight: bold;
}

左右にスワイプ(またはドラッグ)すると、カードが 1 枚ずつ吸着するのがわかります。

mandatory と proximity の違い

scroll-snap-type の第 2 引数には mandatory と proximity の 2 種類があります。どちらを選ぶかで、スクロール時のユーザー体験が大きく変わります。

挙動適したシーン
mandatory必ずスナップするカルーセル、ページ送り
proximity近ければスナップする長いリスト、ギャラリー

mandatory はスクロールが終わると必ず最も近いスナップポイントに吸着します。カルーセルやフルページスクロールのように「中途半端な位置で止まってほしくない」場面に向いています。一方、proximity はスナップポイントの近くで止まったときだけ吸着し、離れた位置では自由に止まれるため、コンテンツ量が多いリストでの使用に適しています。

scroll-snap-align の 3 つの値

子要素に指定する scroll-snap-align は、スナップ時にどの位置に揃えるかを決定します。

start

要素の開始端(横スクロールなら左端、縦スクロールなら上端)がコンテナの開始端に揃います。カードを左揃えで並べたいカルーセルなどに最適です。

center

要素の中心がコンテナの中心に揃います。前後のカードが少し見えるデザインや、フォーカスされたアイテムを中央に表示したい場合に使います。

end

要素の終了端がコンテナの終了端に揃います。右揃え・下揃えのレイアウトで使用しますが、使う場面は比較的限られます。

scroll-padding で余白を調整する

固定ヘッダーがあるページで縦方向の scroll-snap を使うと、スナップ位置がヘッダーの裏に隠れてしまうことがあります。この問題は、コンテナ側に scroll-padding を設定することで解決できます。

.container {
  scroll-snap-type: y mandatory;
  scroll-padding-top: 60px;
}

scroll-padding-top: 60px を指定すると、スナップポイントがコンテナ上端から 60px 内側にずれるため、高さ 60px のヘッダーと重ならなくなります。scroll-padding は上下左右それぞれ個別に指定でき、ショートハンド scroll-padding で一括指定も可能です。

フルページスクロールの実装

scroll-snap を使えば、セクションごとにページが切り替わるフルページスクロールも CSS だけで実現できます。

html {
  scroll-snap-type: y mandatory;
}

section {
  height: 100vh;
  scroll-snap-align: start;
}
HTML
CSS
JavaScript
<div class="fullpage">
  <section class="sec" style="background:#0ea5e9">Section 1</section>
  <section class="sec" style="background:#14b8a6">Section 2</section>
  <section class="sec" style="background:#f59e0b">Section 3</section>
</div>
.fullpage {
  height: 200px;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  border-radius: 8px;
}
.sec {
  height: 200px;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 18px;
  font-weight: bold;
}

各セクションの高さを 100vh にし、scroll-snap-align: start を指定するだけで完成します。ライブラリを使わずに済むため、バンドルサイズの削減にもつながります。

scroll-snap-stop で飛ばし防止

素早くスワイプすると、途中のスナップポイントを飛び越えて一気にスクロールしてしまうことがあります。これを防ぐのが scroll-snap-stop プロパティです。

.card {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

scroll-snap-stop: always を指定すると、どれだけ勢いよくスワイプしても必ず各スナップポイントで一度止まるようになります。チュートリアルのステップ案内やストーリー形式のコンテンツなど、順番に見てほしい場面で効果的です。デフォルト値は normal で、この場合はスナップポイントを飛び越えることが許容されます。

使いどころの注意点

scroll-snap は強力なプロパティですが、使い方を誤るとかえってユーザー体験を損なう場合もあります。

mandatory を長いコンテンツに適用すると、ユーザーが自由にスクロールできず操作感が悪くなることがある
アクセシビリティの観点から、キーボードでのスクロール操作が正しく動作するか確認する
子要素のサイズがコンテナより大きい場合、コンテンツの一部にアクセスできなくなる可能性がある

特に mandatory は強制力が高いため、適用範囲を慎重に検討することが大切です。コンテンツ量が多い場面では proximity を選ぶか、scroll-snap の適用範囲をコンテナ単位に限定するのが安全な設計方針といえます。