スクロールしたときに、要素が中途半端な位置で止まらず、ぴたっと揃う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から試してみるのがおすすめです。

コメントを残す