CSS scroll-snap 実践ガイド ―
JSなしでカルーセル風UIを作る

カルーセルやフルスクリーンスクロールに、もうJavaScriptライブラリは要りません。CSSの scroll-snap だけで、気持ちよく「吸い付く」スクロール体験を実装できます。

scroll-snap とは何か?

Webでカルーセル(スライダー)を作ろうとすると、真っ先に思い浮かぶのはJavaScriptライブラリではないでしょうか。Swiper、Slick、Flickity……。どれも優秀なツールですが、「ただ横にスクロールさせて、ぴったり止めたい」だけのために、数十KBのライブラリを読み込むのは少し大げさです。

実は、CSSだけでスクロール位置を「スナップ」させる仕組みがあります。それが scroll-snap です。親コンテナにスクロールの方向と揃え方を指示し、子要素に「ここで止まれ」というマーカーを付けるだけ。たったこれだけで、スワイプ操作がカチッと決まる気持ちよさを実現できます。

ブラウザ対応も問題ありません。2024年時点ですべてのモダンブラウザが対応しており、現場で安心して使えるプロパティです。

📌 ポイント
scroll-snap は「スクロールの止まり位置をCSSで制御する」機能です。アニメーションやタッチイベントの制御はブラウザが自動で行ってくれるので、開発者はレイアウトに集中できます。

基本の仕組み ― 親と子の2ステップ

scroll-snap の設計はとてもシンプルです。使うプロパティは大きく2つだけ。親要素(スクロールコンテナ)に scroll-snap-type を設定し、子要素(スナップ対象)に scroll-snap-align を設定します。

親要素:scroll-snap-type

scroll-snap-type は、スクロールの方向(x または y)と、スナップの強さ(mandatory または proximity)を指定します。mandatory は「必ずスナップポイントに止まる」、proximity は「近くにスナップポイントがあれば止まる」という意味です。

親要素に設定するCSS
.scroll-container {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  display: flex;
  gap: 16px;
}

子要素:scroll-snap-align

子要素には scroll-snap-align で「どこを基準に揃えるか」を指定します。start(左端・上端に揃える)、center(中央に揃える)、end(右端・下端に揃える)から選べます。

子要素に設定するCSS
.scroll-item {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 280px;
}

これだけで、横スクロール時にアイテムの左端にぴたっと止まるUIが完成します。実際に触ってみましょう。

▶ Live Demo — 基本の横スクロールスナップ
1
2
3
4
5
6

← 横にスクロールしてみてください。各アイテムの左端でぴたっと止まります。

mandatory と proximity の使い分け

scroll-snap を使い始めると最初に迷うのが、mandatoryproximity の違いです。実務でどちらを使うべきかは、UIの用途で決まります。

✅ mandatory — フルスクリーンスクロールやカルーセル向き
.container {
  scroll-snap-type: y mandatory;
  /* 必ずスナップポイントに止まる */
  /* ユーザーが中途半端な位置で止めようとしても引き戻される */
}
✅ proximity — ギャラリーやカード一覧向き
.container {
  scroll-snap-type: x proximity;
  /* 近くにスナップポイントがあれば吸い付く */
  /* 高速スクロール時は自由にスルーできる */
}

ポイントは、mandatory はスクロールの自由を奪う度合いが強い、ということです。フルスクリーンで1ページずつ見せるようなUIでは mandatory が最適ですが、コンテンツが多い横スクロールリストに使うと「快速にスクロールしたいのに引き戻される」とユーザーがストレスを感じることがあります。

💡 現場の経験則
迷ったらまず mandatory で試し、「高速フリックしたとき鬱陶しくないか?」をスマホで確認してみてください。鬱陶しければ proximity に切り替える。このシンプルな判断基準で十分です。
▶ Live Demo — mandatory vs proximity
mandatory(必ず止まる)
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8
proximity(近ければ止まる)
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8

上段(mandatory)は必ずカード端にスナップ。下段(proximity)は勢いよくスクロールするとスナップせずに流れます。

実践パターン ― 現場で本当に使うレイアウト3選

パターン1:カード型カルーセル(scroll-padding 活用)

実務でよくあるのが、「左端にパディングを空けてカードをスナップさせたい」というケースです。デザインカンプでは画面の左右にマージンがあるのに、スクロールコンテナには端までカードを並べたい。そんなときに使うのが scroll-padding です。

scroll-padding でスナップ位置をオフセット
.carousel {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 24px;
  display: flex;
  gap: 16px;
  padding: 0 24px;
}

.carousel-card {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 280px;
}
▶ Live Demo — scroll-padding 付きカルーセル

左端に24pxのパディングが効いた状態でスナップしています。

パターン2:フルスクリーン縦スクロール

LPやプレゼンテーション風のサイトでよく見る「1画面ずつ縦にスクロールする」レイアウトも、scroll-snap で簡単に実現できます。以前は fullPage.js のようなライブラリが必要でしたが、今はCSSだけで十分です。

フルスクリーン縦スナップのCSS
.fullpage {
  height: 100vh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}

.fullpage-section {
  height: 100vh;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
}
▶ Live Demo — フルスクリーン風・縦スナップ
Section 1↓ スクロールしてください
Section 2ぴたっと止まります
Section 3CSSだけでこの動きを実現
Section 4fullPage.js 不要です

高さ300pxのコンテナ内で、セクション単位でスナップします。実際には height: 100vh で全画面にします。

パターン3:center スナップでフォーカスUI

scroll-snap-align: center を使うと、選択されたアイテムが常に中央に来るUIを作れます。画像ギャラリーや、日付ピッカー風のUIなどに最適です。

center スナップの設定
.gallery {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  display: flex;
  gap: 12px;
  padding: 0 calc(50% - 100px);
  /* 最初と最後のアイテムも中央に来るよう左右にパディング */
}

.gallery-item {
  scroll-snap-align: center;
  flex-shrink: 0;
  width: 200px;
}
▶ Live Demo — center スナップギャラリー
A
B
C
D
E

アイテムが必ず中央にスナップします。最初と最後のアイテムも、padding のおかげで中央に来ます。

⚠️ 注意
center スナップで最初・最後のアイテムも中央に持ってくるには、コンテナの paddingcalc(50% - アイテム幅の半分) に設定するのがコツです。これを忘れると、端のアイテムだけ中央に来なくて「あれ?」となります。

scroll-snap-stop で「飛ばし」を防ぐ

mandatory を指定していても、勢いよくフリックすると複数のスナップポイントを飛ばしてしまうことがあります。「必ず1枚ずつ見せたい」というUIでは、子要素に scroll-snap-stop: always を追加します。

1枚ずつ必ず止めたい場合
.card {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* 高速フリックでも必ず各カードで止まる */
}

ただし、scroll-snap-stop: always はユーザーの操作感を大きく制限するので、使う場面は慎重に選んでください。チュートリアルのステップ画面や、注意書きを必ず読ませたいオンボーディングUIなど、「飛ばされると困る」コンテンツに限定するのがおすすめです。

💡 現場の経験則
商品カルーセルや画像ギャラリーで scroll-snap-stop: always を使うのは避けましょう。ユーザーが「早く先に行きたいのに進めない」と感じてしまいます。「1枚ずつ見てほしい」のは作り手の希望であって、ユーザーの希望とは限りません。

レスポンシブ対応とアクセシビリティ

scroll-snap はレスポンシブ対応も簡単です。カード幅をビューポート単位(vw)やパーセンテージで指定すれば、画面サイズに応じた表示ができます。PCでは3枚表示、スマホでは1枚表示、というパターンなら以下のように書きます。

レスポンシブ対応の例
.card {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 85vw;  /* モバイル:ほぼ全幅 */
}

@media (min-width: 768px) {
  .card {
    width: calc(33.333% - 12px);  /* PC:3列 */
  }
}

アクセシビリティの面では、scroll-snap はキーボード操作(Tab + 矢印キー)にも自然に対応します。ただし、スクロールコンテナに tabindex="0"role="region"aria-label を付けておくと、スクリーンリーダーのユーザーにも意味が伝わりやすくなります。

アクセシビリティへの配慮
<div
  class="carousel"
  role="region"
  aria-label="プロジェクト一覧"
  tabindex="0"
>
  <!-- カード -->
</div>

まとめ

  • scroll-snap は親要素に scroll-snap-type、子要素に scroll-snap-align を指定する2ステップで使える
  • mandatory は「必ず止まる」、proximity は「近ければ止まる」。迷ったら mandatory で試してスマホで確認
  • scroll-padding を使えば、デザインカンプ通りのオフセット付きスナップが可能
  • scroll-snap-align: center で、選択アイテムが常に中央に来るフォーカスUIが作れる
  • scroll-snap-stop: always は「飛ばされたら困る」UIだけに限定して使う
  • レスポンシブ対応はカード幅を vw や % で指定するだけ。メディアクエリとの相性も良い
  • role="region" と aria-label を加えることで、アクセシビリティにも対応できる