Scroll Snap Animationで気持ちよく止まるUIを作る方法【CSS中心で実装】

Scroll Snap Animationのアイキャッチ画像

スクロールしたときに、要素が中途半端な位置で止まらず、ぴたっと揃うUIを見たことはないでしょうか。

たとえば、

  • 横にスライドするカード一覧
  • 1画面ずつ切り替わるセクション
  • スマホでのカルーセル風レイアウト

スクロール量そのものを視覚化したい場合は、あわせて Scroll Progress の実装パターンも見ておくと、スクロールUI全体の設計を考えやすくなります。

このようなUIでは、通常のスクロールよりも Scroll Snap を使ったほうが、整った見た目と気持ちよい操作感を作れます。

この記事では、CSS Scroll Snap を使って、
スクロールに合わせて要素が吸い付くように止まるUIを作る方法を、完全コピペで試せる形で紹介します。

JavaScriptをほとんど使わずに実装できるので、初心者でも導入しやすいパターンです。

1.Scroll Snap Animationとは?

Scroll Snap Animation は、スクロール時に要素が決められた位置に自動で揃う動きを活かしたUIパターンです。

たとえば横スクロールのカードで使うと、

  • 少しだけずれて止まる
  • カードの途中で止まって見切れる
  • 操作感がなんとなく中途半端

といった状態を防げます。

Scroll Snapを有効にすると、スクロール後にブラウザが近いスナップ位置へ補正してくれるため、
UI全体がすっきり見え、操作体験も安定します。

スクロールに応じて「止まり方」を整えるのが今回のテーマなら、要素の「現れ方」を整える Scroll Reveal Animation も相性の良いパターンです。

2.まずは完成形

今回作るのは、横スクロールのカードが1枚ずつ気持ちよく揃うUIです。

ポイントは次の3つです。

  • 親要素に scroll-snap-type
  • 子要素に scroll-snap-align
  • スクロールしやすい余白とサイズ設計

まずはコード全体を見て、そのあとで仕組みを分解していきましょう。

3.完全コピペコード

HTML

<section class="snap-demo">
  <div class="snap-header">
    <p class="eyebrow">Scroll Snap Animation</p>
    <h2>カードがぴたっと揃う横スクロールUI</h2>
    <p class="lead">
      横にスクロールすると、カードが中途半端な位置で止まらず、
      1枚ずつ気持ちよく揃います。
    </p>
  </div>

  <div class="snap-track" id="snapTrack">
    <article class="snap-card">
      <span class="num">01</span>
      <h3>Welcome</h3>
      <p>スクロールに合わせてカードが吸い付くように止まる基本デモです。</p>
    </article>

    <article class="snap-card">
      <span class="num">02</span>
      <h3>Smooth Layout</h3>
      <p>中途半端な位置で止まらないので、一覧UIが整って見えやすくなります。</p>
    </article>

    <article class="snap-card">
      <span class="num">03</span>
      <h3>Touch Friendly</h3>
      <p>スマホのスワイプ操作とも相性が良く、自然な体験を作りやすいです。</p>
    </article>

    <article class="snap-card">
      <span class="num">04</span>
      <h3>Minimal JS</h3>
      <p>基本はCSSだけで成立するので、実装コストを抑えやすいのも魅力です。</p>
    </article>

    <article class="snap-card">
      <span class="num">05</span>
      <h3>UI Pattern</h3>
      <p>チュートリアル、機能紹介、ギャラリーなど幅広い場面で使えます。</p>
    </article>
  </div>

  <div class="snap-controls">
    <button type="button" id="prevBtn">Prev</button>
    <button type="button" id="nextBtn">Next</button>
  </div>
</section>

CSS

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background:
    radial-gradient(circle at top, rgba(120, 255, 210, 0.08), transparent 35%),
    linear-gradient(180deg, #0d1016 0%, #111827 100%);
  color: #eef2ff;
}

.snap-demo {
  width: min(1100px, calc(100% - 32px));
  margin: 64px auto;
}

.snap-header {
  margin-bottom: 20px;
}

.eyebrow {
  margin: 0 0 8px;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #8ef0c9;
}

.snap-header h2 {
  margin: 0;
  font-size: clamp(28px, 4vw, 44px);
  line-height: 1.15;
}

.lead {
  margin: 12px 0 0;
  max-width: 720px;
  color: #cbd5e1;
  line-height: 1.7;
}

.snap-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(82%, 420px);
  gap: 16px;
  overflow-x: auto;
  padding: 8px 8px 20px;
  margin-top: 28px;

  scroll-snap-type: x mandatory;
  scroll-padding-left: 8px;
  scroll-behavior: smooth;

  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.28) transparent;
}

.snap-track::-webkit-scrollbar {
  height: 10px;
}

.snap-track::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.24);
  border-radius: 999px;
}

.snap-card {
  min-height: 260px;
  padding: 24px;
  border-radius: 24px;
  background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.04));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);

  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.snap-card .num {
  display: inline-block;
  margin-bottom: 18px;
  font-size: 13px;
  letter-spacing: 0.12em;
  color: #8ef0c9;
}

.snap-card h3 {
  margin: 0 0 12px;
  font-size: 24px;
}

.snap-card p {
  margin: 0;
  line-height: 1.75;
  color: #dbe4f0;
}

.snap-controls {
  display: flex;
  gap: 12px;
  margin-top: 20px;
}

.snap-controls button {
  appearance: none;
  border: 0;
  border-radius: 999px;
  padding: 12px 18px;
  font: inherit;
  font-weight: 600;
  color: #0f172a;
  background: #8ef0c9;
  cursor: pointer;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.snap-controls button:hover {
  transform: translateY(-1px);
}

.snap-controls button:active {
  transform: translateY(1px) scale(0.98);
}

@media (max-width: 640px) {
  .snap-demo {
    width: min(100% - 20px, 1100px);
    margin: 40px auto;
  }

  .snap-track {
    grid-auto-columns: 88%;
    gap: 12px;
  }

  .snap-card {
    min-height: 220px;
    padding: 20px;
    border-radius: 20px;
  }
}

JavaScript

<script>
  const track = document.getElementById("snapTrack");
  const cards = Array.from(track.children);
  const prevBtn = document.getElementById("prevBtn");
  const nextBtn = document.getElementById("nextBtn");

  function getCurrentIndex() {
    const trackLeft = track.getBoundingClientRect().left;

    let closestIndex = 0;
    let closestDistance = Infinity;

    cards.forEach((card, index) => {
      const cardLeft = card.getBoundingClientRect().left;
      const distance = Math.abs(cardLeft - trackLeft - 8);

      if (distance < closestDistance) {
        closestDistance = distance;
        closestIndex = index;
      }
    });

    return closestIndex;
  }

  function scrollToCard(index) {
    const safeIndex = Math.max(0, Math.min(index, cards.length - 1));
    cards[safeIndex].scrollIntoView({
      behavior: "smooth",
      inline: "start",
      block: "nearest"
    });
  }

  prevBtn.addEventListener("click", () => {
    scrollToCard(getCurrentIndex() - 1);
  });

  nextBtn.addEventListener("click", () => {
    scrollToCard(getCurrentIndex() + 1);
  });
</script>

4.どういう仕組みなのか

Scroll Snapの基本はとてもシンプルです。

親要素に scroll-snap-type を付ける

.snap-track {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

これで、横方向のスクロールに対してスナップが有効になります。

  • x は横方向
  • mandatory は、スクロール後に必ずスナップ位置へ寄せる設定

まずはこの指定が土台になります。

子要素に scroll-snap-align を付ける

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

これは、各カードのどの位置をスナップ基準にするかの指定です。

  • start : 先頭位置で揃える
  • center : 中央で揃える
  • end : 終端で揃える

一覧の読みやすさを重視するなら、まずは start が使いやすいです。

scroll-snap-stop: always; で止まりやすくする

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

これを入れておくと、勢いよくスクロールしたときでもカードを飛ばしにくくなります。

環境や操作方法によって体感は変わりますが、
「1枚ずつ区切って見せたい」UIでは相性が良い指定です。

5.Scroll Snapを使うメリット

Scroll Snapの良さは、単に「ぴたっと止まる」だけではありません。

レイアウトが整って見える

カード一覧が毎回きれいに揃うので、UI全体にまとまりが出ます。

実装コストが低い

基本はCSSだけで成立するため、JavaScriptの制御を最小限にできます。

スマホ操作と相性が良い

スワイプでカードを送るような体験と自然に組み合わせられます。

カルーセルの簡易版として使いやすい

大がかりなライブラリを入れなくても、軽量な横スクロールUIを作れます。

カードUIをさらに気持ちよく見せたいなら、表示タイミングを少しずつずらす Stagger Animation と組み合わせるのもおすすめです。

6.実装時の注意点

便利なScroll Snapですが、いくつか意識しておくと仕上がりが安定します。

カード幅を広すぎなくする

1枚が画面幅ギリギリすぎると、操作しにくく見えることがあります。
少し次のカードが見えるくらいにすると、「横に続いている」ことが伝わりやすいです。

今回のコードでは次のようにしています。

grid-auto-columns: min(82%, 420px);

これで、環境に応じてちょうどよい横幅に調整しやすくなります。

余白もスナップ体験に影響する

先頭カードが端に張り付きすぎると、少し窮屈に見えます。
そのため、padding と scroll-padding-left を合わせて調整しています。

padding: 8px 8px 20px;
scroll-padding-left: 8px;

縦スクロールページの中で使うときは入れ子に注意

横スクロールUIをページ内に置く場合、親子のスクロールが干渉すると操作しづらくなることがあります。
スマホでの体験を見ながら、余白や高さを調整すると安定しやすいです。

7.ボタン操作も加える理由

今回は Prev / Next ボタンも付けています。

Scroll Snapはスクロール操作だけでも十分使えますが、
ボタンがあると次のような利点があります。

  • マウス中心の環境でも操作しやすい
  • 1枚ずつ送る意図がわかりやすい
  • デモとして挙動を確認しやすい

実際の制作では、

  • スマホ中心ならスクロールのみ
  • PCでも見せたいならボタン付き

という使い分けがしやすいです。

8.実験:Scroll Snapで止まり方を比較してみる

このデモでは、横にスクロールしたときにカードが近い位置へ吸い付くように揃う感覚を確認できます。

特に見てほしいポイントは次の3つです。

  • カードが中途半端な位置で止まりにくいこと
  • 少し次のカードが見えることで、続きがあるとわかること
  • ボタン操作でも同じように整って移動できること

観察ポイント

1. 通常の横スクロールより整って見えるか

一覧UIは、少しのズレでも雑に見えやすいです。
Scroll Snapが入ると、止まる位置が揃うだけで印象が変わります。

2. 情報の区切りが見やすいか

カード単位で区切って見せたいとき、スナップはかなり相性が良いです。
「今どのカードを見ているか」が分かりやすくなります。

3. CSS中心で十分か

この種のUIは、ライブラリを使わなくても成立する場面が多いです。
まずはScroll Snapだけで作れるか考えると、実装が軽くまとまりやすいです。

どんな場面で使いやすい?

Scroll Snap Animation は、特に次のような場面で使いやすいです。

  • 機能紹介カード
  • 作品ギャラリー
  • チュートリアルのステップ表示
  • おすすめ記事の横並びリスト
  • LPのセクション送りUI

「連続した情報を、1ブロックずつ気持ちよく見せたい」ときに向いています。

スクロールを使ったUIでは、「今どこまで進んだか」を示す演出を足すだけでも使いやすさが上がるので、必要に応じて Scroll Progress のような補助UIも検討できます。

9.まとめ

Scroll Snap Animation は、少ないコードで操作感を大きく改善しやすいUIパターンです。

今回のポイントをまとめると、次の通りです。

  • 親要素に scroll-snap-type
  • 子要素に scroll-snap-align
  • 必要に応じて scroll-snap-stop
  • ボタン操作を足すとPCでも扱いやすい

派手なアニメーションではありませんが、
UIの気持ちよさを底上げしてくれる実用的なテクニックです。

横スクロール一覧やカルーセル風UIを作るときは、
まずこのScroll Snapから試してみるのがおすすめです。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA