



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒スマホでカードを横スクロールさせたいんですが、指を離すと中途半端な位置で止まります…。
ペン博士それはscroll-snapで解決できるよ。数行のCSSで、カードがぴたっと定位置に吸い付くようになる。カルーセルもライブラリなしで作れるんだ。
スマホで人気の横スクロールUI。実装はoverflow-x: autoだけでも動きますが、それだけだと中途半端な位置で止まってカードが見切れます。
scroll-snapを使えば、スクロールの停止位置をCSSで制御できます。JavaScriptのカルーセルライブラリを入れなくても、指を離した瞬間に定位置へ吸い付く動きが作れます。
この記事では基本の2プロパティから、横カルーセル・全画面セクション・サムネイル一覧の実装、はまりどころまで解説します。

覚えるのは2つだけです。スクロールする親に「型」を、止めたい子に「位置」を指定します。
/* 親:どの方向に、どれくらい強く吸い付かせるか */
.snap {
overflow-x: auto;
scroll-snap-type: x mandatory;
}
/* 子:どこで止めるか */
.snap__item {
scroll-snap-align: start;
}| プロパティ | 値 | 意味 |
|---|---|---|
scroll-snap-type | x / y / both | スナップさせる方向 |
| mandatory | 必ず定位置に吸い付く | |
| proximity | 近いときだけ吸い付く | |
scroll-snap-align | start | 要素の先頭で止める |
| center | 中央で止める | |
| end | 末尾で止める |
迷ったらx mandatory と startの組み合わせです。カードを左端に揃えて止める、最も自然な動きになります。

最も使う形です。カードを横に並べ、1枚ずつ吸い付くようにします。
<div class="carousel">
<article class="carousel__item">カード1</article>
<article class="carousel__item">カード2</article>
<article class="carousel__item">カード3</article>
<article class="carousel__item">カード4</article>
</div>.carousel {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-left: 20px; /* 左端に残す余白 */
padding: 4px 20px 20px;
-webkit-overflow-scrolling: touch;
}
.carousel__item {
flex: 0 0 78%; /* 次のカードを少し見せる幅 */
max-width: 320px;
scroll-snap-align: start;
padding: 20px;
border-radius: 14px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(15, 23, 42, .08);
}
@media (min-width: 768px) {
.carousel__item {
flex-basis: 32%; /* PCでは3枚見せる */
}
}幅を78%にするのが要点です。次のカードが少しだけ見える状態にすると、横に続きがあることが直感的に伝わります。100%にすると気づかれません。
ヒーローエリアや商品紹介で使う、1画面ずつ切り替わる形です。子要素を画面幅にするだけで作れます。
.slider {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth; /* リンク移動を滑らかに */
}
.slider__slide {
flex: 0 0 100%;
min-height: 60svh;
scroll-snap-align: center;
scroll-snap-stop: always; /* 勢いで飛ばさせない */
display: grid;
place-items: center;
}scroll-snap-stop: alwaysを入れると、勢いよくスワイプしても1枚ずつ止まります。これが無いと2〜3枚まとめて飛ばされ、見せたい内容が読まれません。
横スクロールUIではバーが目立ちすぎることがあります。ただし完全に消すと、スクロールできること自体が伝わらなくなります。
/* 細く目立たなくする(推奨) */
.carousel {
scrollbar-width: thin; /* Firefox */
scrollbar-color: #cbd5e1 transparent;
}
.carousel::-webkit-scrollbar {
height: 6px;
}
.carousel::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 999px;
}
/* 完全に隠す(見切れ演出とセットで使う) */
.carousel.-hide-bar {
scrollbar-width: none;
}
.carousel.-hide-bar::-webkit-scrollbar {
display: none;
}バーを消す場合は、必ずカードが端で見切れる形にしてください。見切れが「まだ続く」というサインになります。

見落としやすいのが余白です。paddingだけでは、スナップした位置がずれます。
.carousel {
padding-inline: 20px; /* 見た目の余白 */
scroll-padding-inline: 20px; /* スナップ位置の余白(必須) */
}
/* 中央寄せで止める場合 */
.carousel.-center {
scroll-snap-type: x mandatory;
scroll-padding-inline: 50%;
}
.carousel.-center .carousel__item {
scroll-snap-align: center;
}scroll-paddingはスナップ位置の基準をずらす指定です。これが無いと、カードが画面端にぴったり付いて窮屈に見えます。paddingと同じ値を入れておけば揃います。
縦にもスナップできますが、使い方を誤ると読みづらいページになります。
.sections {
height: 100svh;
overflow-y: auto;
scroll-snap-type: y proximity; /* mandatoryより穏やか */
}
.sections__block {
min-height: 100svh;
scroll-snap-align: start;
display: grid;
place-items: center;
}縦方向ではproximity を選ぶのが安全です。mandatoryだと、セクションより長い文章があるときに途中で読めなくなり、スクロールが乗っ取られたように感じられます。
何枚あるか、今どこかを示す点を足すと親切です。リンクとして作れば移動もできます。
<div class="carousel">
<article id="slide1" class="carousel__item">カード1</article>
<article id="slide2" class="carousel__item">カード2</article>
<article id="slide3" class="carousel__item">カード3</article>
</div>
<nav class="pager">
<a href="#slide1">1枚目へ</a>
<a href="#slide2">2枚目へ</a>
<a href="#slide3">3枚目へ</a>
</nav>.pager {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 12px;
}
.pager a {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #cbd5e1;
text-indent: -9999px; /* 文字は読み上げ用に残す */
overflow: hidden;
transition: background-color .2s, transform .2s;
}
.pager a:hover,
.pager a:focus-visible {
background-color: #0ea5e9;
transform: scale(1.2);
}テキストをtext-indentで画面外に出すと、見た目は点のまま読み上げには意味が伝わります。単なるspanで作るより親切です。

スナップしない場合、原因はほぼ次の6つです。
| 症状 | 原因 | 対処 |
|---|---|---|
| まったく吸い付かない | 親にoverflowがない | overflow-x: autoを指定 |
| 子が止まらない | scroll-snap-align漏れ | 各子要素に指定する |
| カードが縮む | flexの初期値 | flex: 0 0 幅を指定 |
| 位置が端に寄りすぎる | scroll-padding未指定 | paddingと同じ値を入れる |
| 何枚も飛ばされる | 勢いが強い | scroll-snap-stop: always |
| 縦で読めなくなる | y mandatory | proximityに変更 |
最も多いのは3番目です。Flexboxの子は既定で縮むため、flex: 0 0 幅 で縮小を止める指定が必須になります。
横スクロールは便利ですが、万能ではありません。導入前に次の点を確認してください。
/* PCでは横スクロールをやめてグリッドにする */
@media (min-width: 1024px) {
.carousel {
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow-x: visible;
scroll-snap-type: none;
}
.carousel__item {
max-width: none;
}
}スマホは横スクロール、PCはグリッド。この出し分けが最も無難で、どちらの環境でも使いやすくなります。
スマホは横カルーセル、PCは3列グリッドに切り替わる一式です。
.carousel {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 20px;
padding: 4px 20px 20px;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.carousel::-webkit-scrollbar {
height: 6px;
}
.carousel::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 999px;
}
.carousel__item {
flex: 0 0 78%;
max-width: 320px;
scroll-snap-align: start;
scroll-snap-stop: always;
padding: 20px;
border-radius: 14px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(15, 23, 42, .08);
}
@media (min-width: 1024px) {
.carousel {
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow-x: visible;
scroll-snap-type: none;
padding: 4px 0 0;
}
.carousel__item {
flex: initial;
max-width: none;
}
}横スクロールが活きる代表的な3パターンを、コードごと置いておきます。
.thumbs {
display: flex;
gap: 10px;
overflow-x: auto;
scroll-snap-type: x proximity;
padding-bottom: 10px;
}
.thumbs img {
flex: 0 0 120px;
height: 84px;
object-fit: cover;
border-radius: 8px;
scroll-snap-align: center;
cursor: pointer;
transition: outline-color .2s;
outline: 3px solid transparent;
}
.thumbs img:hover,
.thumbs img:focus-visible {
outline-color: #0ea5e9;
}サムネイルはproximity と centerの組み合わせが快適です。厳密に1枚ずつ止める必要がなく、流し見しながら選べます。
.table-scroll {
overflow-x: auto;
scroll-snap-type: x proximity;
scroll-padding-left: 140px; /* 固定列のぶんを避ける */
}
.table-scroll th:first-child,
.table-scroll td:first-child {
position: sticky;
left: 0;
width: 140px;
background-color: #fff; /* 背景がないと透ける */
z-index: 1;
}
.table-scroll th:not(:first-child) {
scroll-snap-align: start;
min-width: 160px;
}項目名の列をposition: stickyで固定し、比較対象の列だけをスナップさせると、スマホでも比較しやすい表になります。固定列には必ず背景色を指定してください。
.date-strip {
display: flex;
gap: 8px;
overflow-x: auto;
scroll-snap-type: x mandatory;
padding: 8px 16px;
}
.date-strip__item {
flex: 0 0 68px;
scroll-snap-align: center;
padding: 10px 0;
border-radius: 10px;
border: 2px solid #e5e7eb;
background-color: #fff;
text-align: center;
line-height: 1.4;
}
.date-strip__item.-selected {
border-color: #0ea5e9;
background-color: #f0f9ff;
color: #0369a1;
font-weight: 700;
}予約フォームなどで使う形です。選択中の項目を色で示し、初期表示時に該当位置までスクロールしておくと親切です。
scroll-snapは主要ブラウザで広く使えます。導入前に、ライブラリと何が違うのかを整理しておきます。
| 観点 | scroll-snap(CSS) | JSライブラリ |
|---|---|---|
| 読み込み | 追加ファイルなし | 数十KB〜 |
| 慣性の挙動 | OS標準そのまま | 独自実装で統一 |
| 自動再生 | できない | できる |
| 無限ループ | できない | できる |
| 矢印ボタン | リンクで代替 | 標準機能 |
| 表示崩れのリスク | 低い | 初期化前に崩れることがある |
自動再生や無限ループが要件でなければ、CSSだけで十分です。読み込むファイルが減るぶん表示も速く、初期化前のちらつきも起きません。
/* 未対応環境でも横スクロールとしては機能する */
@supports not (scroll-snap-type: x mandatory) {
.carousel__item {
flex-basis: 70%; /* 吸い付かないぶん、見切れを強めにする */
}
}スナップが効かない環境でも、横スクロール自体は動きます。段階的な体験の低下で済むため、フォールバックの心配はほとんどありません。
横スクロールUIは画像を多く並べがちです。表示速度を落とさないための注意点をまとめます。
loading="lazy"を付け、初期表示の負荷を下げるaspect-ratioでレイアウト移動(CLS)を防ぐ<article class="carousel__item">
<img src="/img/case01.webp" alt="制作事例1" width="640" height="360" loading="lazy">
<h3>事例タイトル</h3>
</article>widthとheightを属性で書いておくと、読み込み前から場所が確保されカクつきが消えます。地味ですが体感速度に効きます。
実装は数行で終わるぶん、設計判断のほうが結果を左右します。着手前に次の4点を決めておくと、後戻りがありません。
1画面に何枚見せるかで、カード幅も文字量も変わります。スマホで1.3枚、タブレットで2.2枚、PCで3枚が扱いやすい基準です。小数を含む枚数にするのは、次のカードを見切れさせるためです。
.carousel__item { flex: 0 0 78%; } /* スマホ:1.3枚 */
@media (min-width: 768px) { .carousel__item { flex-basis: 45%; } } /* 2.2枚 */
@media (min-width: 1024px) { .carousel__item { flex-basis: 32%; } } /* 3枚 */横スクロールは2枚目以降の閲覧率が大きく落ちます。最も伝えたい事例や、最も売りたいプランは必ず1枚目に置いてください。並び順を更新頻度で決めると、重要な情報が埋もれます。
カルーセルは「一部を見せる」ための形式です。10枚を超える場合は、末尾に一覧ページへのリンクカードを置くか、見出し横に「すべて見る」を添えます。
<article class="carousel__item -more">
<a href="/works/">制作事例をすべて見る</a>
</article>導入後は、2枚目以降が見られているかを確認します。スクロール到達率をイベントとして送れば、枚数や順番の判断材料になります。見られていないなら、枚数を減らすかグリッド表示に戻すのが正解です。
| 確認すること | 見る指標 | 改善の方向 |
|---|---|---|
| 続きが見られているか | スクロール到達率 | 見切れ幅を増やす |
| カードが押されているか | クリック率 | 1枚目の内容を変える |
| 離脱していないか | 直帰率 | 枚数を減らす |
| 表示が重くないか | LCP・CLS | 遅延読み込みと比率固定 |
横スクロールは省スペースと引き換えに閲覧率を犠牲にする形式です。効果を測らずに置き続けるのではなく、数字を見て残すか戻すかを決めてください。
scroll-snapは、スクロールする親にscroll-snap-type、止めたい子にscroll-snap-alignを書くだけで使えます。横カルーセルではカード幅を78%前後にして次の1枚を見切れさせ、flex: 0 0 幅で縮小を止めるのが要点です。端の余白はpaddingとscroll-paddingを同じ値で揃え、勢いで飛ばしたくない場面ではscroll-snap-stop: alwaysを足します。縦方向はproximityを選び、PCではグリッドに戻す出し分けまで入れておけば、どの環境でも扱いやすいUIになります。
スクロールする親要素にoverflow-x: auto(縦ならoverflow-y)が指定されているか確認してください。スクロールが発生しない要素にはスナップも効きません。加えて、各子要素にscroll-snap-alignが必要です。親だけの指定では動きません。
カードを1枚ずつ確実に見せたい横カルーセルではmandatoryが適しています。縦方向のセクション送りではproximityを推奨します。mandatoryは必ず定位置へ吸い付くため、セクション内に長い文章があると途中で読めなくなります。
Flexboxの子要素は既定で縮むためです。flex: 0 0 78%のように縮小を0にして幅を固定してください。widthだけの指定では縮小が優先され、カードが潰れて並びます。
消す場合はカードを画面端で見切れさせ、横に続きがあると分かる形にしてください。バーも見切れもない状態では、スクロールできること自体に気づかれません。細く目立たない見た目に変えるだけの対応も有効です。
マウスでの横スクロールは操作しにくいため、1024px以上ではグリッド表示に切り替えるのが親切です。display: gridとscroll-snap-type: noneに戻すだけで実現できます。
CSSの実装テクニックを「レイアウト・装飾・アニメーション・基礎・トラブル解決」の目的別に探せる総まとめページを用意しています。実装で迷ったときの索引としてどうぞ。

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。
会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!
WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。
公式サイト より
今すぐ
無料カウンセリング
を予約!