CSSで、横スクロールのカードをぴたっと止める方法
横に並べたカードを、スクロールし終えたときにカードの左端でぴたっと止めるには、CSS の scroll-snap を使います。JavaScript は要りません。リストに scroll-snap-type、カードに scroll-snap-align を付けるのが基本で、止まる位置をリストの余白に合わせたいときは scroll-padding-inline、強く払っても1枚ずつ進ませたいときは scroll-snap-stop を足します。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。幅360pxの画面で HTML と CSS のページを開き、指の操作を Chrome DevTools Protocol のタッチイベントで再現して、止まった位置(scrollLeft)を記録しました。Firefox と Safari、スマートフォンの実機、トラックパッドやマウスホイールでの操作は試していません。

リストに scroll-snap-type、カードに scroll-snap-align を付ける
HTML は、カードを <ul> に並べただけです。
<section aria-label="新着の記事">
<ul class="card-list">
<li class="card"><h3>カード 1</h3><p>横にスクロールすると、カードの左端で止まります。</p></li>
<li class="card"><h3>カード 2</h3><p>次のカードが右に少し見えるので、続きがあると分かります。</p></li>
<!-- カード 3〜6 も同じ形 -->
</ul>
</section>
CSS は次のとおりです。
/* 横スクロールで、カードの左端にぴたっと止まるリスト */
.card-list {
display: flex;
gap: 12px;
margin: 0;
padding: 0 16px 12px;
list-style: none;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 16px;
}
.card {
flex: 0 0 75%;
scroll-snap-align: start;
box-sizing: border-box;
padding: 16px;
border: 1px solid #b8e6ef;
border-radius: 12px;
background: #eefafd;
}
.card h3 {
margin: 0 0 8px;
font-size: 18px;
color: #067a98;
}
.card p {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: #1f2a30;
}
止まる動きを決めているのは、次の3行です。
- リストの
scroll-snap-type: x mandatory:横方向に、必ずどこかの止まる位置に合わせる - カードの
scroll-snap-align: start:カードの左端(先頭)を、止まる位置にする - リストの
scroll-padding-inline: 16px:止まる位置を、リストの左端から16px内側にずらす
scroll-snap-type の説明によると、mandatory はスクロールしていないときに必ず止まる位置に合わせる値で、proximity は止まる位置に合わせるかどうかをブラウザに任せる値です。止まる位置は、scroll-snap-align の start、center、end で、カードの先頭、中央、末尾のどれに合わせるかを選びます。
カードの幅を flex: 0 0 75% にしているのは、次のカードを右に少し見せて、続きがあると分かるようにするためです。幅360pxの画面では、リストの中身の幅(360pxから左右の余白を引いた328px)の75%で、カードの幅は246pxになりました。
指で動かして離すと、近いほうのカードに止まる
指を左へ、40msおきに12回に分けて動かし、離したあとに止まった位置を記録しました。比べるために、scroll-snap-type: none にした版も試しています。
| 指で動かした距離 | x mandatory(記事の CSS) |
none |
|---|---|---|
| 60px | 0(カード1の左端) | 45 |
| 160px | 258(カード2の左端) | 145 |
| 330px | 258(カード2の左端) | 315 |
数字は止まったときの scrollLeft です。カード1枚とすき間(246px+12px)で、止まる位置は258pxずつ並びます。x mandatory では、どの距離でも0か258で止まり、カードの途中で止まることはありませんでした。60pxでは元のカード1に戻り、160pxではカード2に進みました。330px動かしたときは、カード2(258)とカード3(516)のうち近いカード2に戻っています。none の版では、指を離したところで止まり、カードが左端で切れたままになりました。
止まる位置へは、一度に飛ぶのではなく、少しずつ動きます。160px動かして離したあと、50msおきに scrollLeft を読むと、151、201、243、258と増えて止まりました。冒頭の GIF は、同じような操作を100px、160px、60pxの順で撮ったものです。
スクリプトで途中の位置へ動かしたときも、同じように止まる位置に合わせられました。scrollTo({ left: 100 }) では scrollLeft が0に、scrollTo({ left: 140 }) では258になりました。none の版では、指定した100や140のままです。
止まる位置を、リストの左の余白に合わせる
リストの左右に16pxの padding を付けているので、最初のカードは左端から16px内側に並びます。ところが scroll-padding-inline を付けないと、スクロールして止まったときのカードの左端は、リストの左端ぴったり(0px)になりました。最初のカードも、60px動かして離すと scrollLeft が16になり、左の余白がなくなりました。
scroll-padding-inline: 16px を付けると、止まる位置がリストの左端から16px内側になり、どのカードに止まっても左に16pxの余白が残りました。scroll-padding-inline の説明にあるとおり、この値は、リストの見えている範囲(スクロールポート)の左右の辺から、止まる位置までの距離です。リストの padding と同じ値にしておくと、最初のカードとそれ以降のカードで、左の余白がそろいます。

上の段で、カード2の左の余白にカード1の右端が4pxだけ見えているのは、すき間(12px)が余白(16px)より狭いからです。gap を16pxにして scroll-padding-inline と同じ値にすると、カード1の右端はリストの左端ちょうど(0px)になり、前のカードは見えなくなりました。
払っても1枚ずつ進ませるなら scroll-snap-stop: always
強く払ったときは、勢いで何枚か先まで進むことがあります。1回の操作で1枚ずつ進ませたいときは、カードに scroll-snap-stop: always を付けます。
.card {
scroll-snap-stop: always;
}
scroll-snap-stop の説明では、always は止まる位置を「通り過ぎる」ことを許さない値で、初期値の normal は通り過ぎてもよい値です。
指を左へ200px、16msおきに3回に分けて動かして離す操作を、normal と always でそれぞれ10回試しました。
normal(初期値) |
always |
|
|---|---|---|
| カード2(258)で止まった回数 | 5回 | 10回 |
| カード3(516)まで進んだ回数 | 5回 | 0回 |
scrollBy({ left: 700 }) で止まった位置 |
774(カード4) | 258(カード2) |
normal では、同じ操作でもカード2で止まるときとカード3まで進むときがありました。always では、10回ともカード2で止まりました。スクリプトの scrollBy({ left: 700 }) でも、normal は700に近いカード4へ動き、always はカード2で止まりました。
最後のカードは、右端まで来たところで止まる
scroll-snap-align: start にしていても、最後のカードは左端までは来ません。それ以上右へスクロールできないからです。右端までスクロールすると、scrollLeft は最大の1208で止まり、カード6の左端はリストの左端から98pxの位置、右には16pxの余白が残りました。
カードを画面の真ん中に止めたいときは、scroll-snap-align: center にします。この版で、指を左へゆっくり200pxずつ動かして離すと、カード2〜5では左右に57pxずつの余白ができて、カードが中央で止まりました。カード1とカード6は、それ以上スクロールできないため中央には来ず、start の場合と同じ位置(カード1は左の余白が16px、カード6は右の余白が16px)で止まりました。
キーボードの矢印キーでも、カード1枚ずつ動く
リストに tabindex を付けなくても、Chromium 153 では Tab キーでリストにフォーカスが移りました。そのまま右の矢印キーを押すと scrollLeft が258、516と、カード1枚ずつ動いて左端に止まり、左の矢印キーで258に戻りました。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で、この記事で使ったプロパティを調べました。
| プロパティ | Chrome | Firefox | Safari | iOS の Safari |
|---|---|---|---|---|
scroll-snap-type |
69 | 99 | 11 | 11 |
scroll-snap-align |
69 | 68 | 11 | 11 |
scroll-snap-stop |
75 | 103 | 15 | 15 |
scroll-padding-inline |
69 | 68 | 15 | 15 |
scroll-snap-stop と scroll-padding-inline は、Safari では15からの対応です。scroll-snap-type と scroll-snap-align は、Safari 11から対応しています。





