WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

scroll-snapで作る横スクロールUI|カルーセルをCSSだけで実装

生徒

スマホでカードを横スクロールさせたいんですが、指を離すと中途半端な位置で止まります…。

ペン博士

それはscroll-snapで解決できるよ。数行のCSSで、カードがぴたっと定位置に吸い付くようになる。カルーセルもライブラリなしで作れるんだ。

スマホで人気の横スクロールUI。実装はoverflow-x: autoだけでも動きますが、それだけだと中途半端な位置で止まってカードが見切れます。

scroll-snapを使えば、スクロールの停止位置をCSSで制御できます。JavaScriptのカルーセルライブラリを入れなくても、指を離した瞬間に定位置へ吸い付く動きが作れます。

この記事では基本の2プロパティから、横カルーセル・全画面セクション・サムネイル一覧の実装、はまりどころまで解説します。

  • scroll-snapの基本(親と子で指定する2つ)
  • 横スクロールカルーセルの作り方
  • 1画面ずつ送るフルスクリーンスライダー
  • スクロールバーの見せ方と隠し方
  • 端の余白(padding)の正しい取り方
  • 縦方向スナップの使いどころ
  • ページャー(インジケーター)の追加
  • 効かないときの原因と対処
  • 使いすぎに注意したいUX上の観点
  • コピペで使える完成版

目次

scroll-snapの基本

スナップの強さの違い

覚えるのは2つだけです。スクロールする親に「型」を、止めたい子に「位置」を指定します。

/* 親:どの方向に、どれくらい強く吸い付かせるか */
.snap {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

/* 子:どこで止めるか */
.snap__item {
  scroll-snap-align: start;
}
プロパティ意味
scroll-snap-typex / y / bothスナップさせる方向
mandatory必ず定位置に吸い付く
proximity近いときだけ吸い付く
scroll-snap-alignstart要素の先頭で止める
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画面ずつ送るフルスクリーンスライダー

ヒーローエリアや商品紹介で使う、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)の正しい取り方

余白の指定は2種類

見落としやすいのが余白です。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 mandatoryproximityに変更

最も多いのは3番目です。Flexboxの子は既定で縮むため、flex: 0 0 幅 で縮小を止める指定が必須になります。

使いすぎに注意したいUX上の観点

横スクロールは便利ですが、万能ではありません。導入前に次の点を確認してください。

  • PCでは操作しにくい:マウスでの横スクロールは手間。PCでは通常のグリッドに戻すのが親切
  • 見えない情報は読まれない:重要な項目は1枚目に置く
  • 件数が多すぎない:目安は10枚以内。それ以上は一覧ページへ誘導する
  • キーボード操作:カード内にリンクがあればTabキーで移動でき、自動でスクロールされる
/* 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種(そのまま使える例)

横スクロールが活きる代表的な3パターンを、コードごと置いておきます。

1. サムネイルギャラリー

.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枚ずつ止める必要がなく、流し見しながら選べます。

2. 比較テーブルの横スクロール

.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で固定し、比較対象の列だけをスナップさせると、スマホでも比較しやすい表になります。固定列には必ず背景色を指定してください。

3. 日付・時間帯の選択UI

.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;
}

予約フォームなどで使う形です。選択中の項目を色で示し、初期表示時に該当位置までスクロールしておくと親切です。

ブラウザ対応とJavaScriptライブラリとの比較

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)を防ぐ
  • 影とぼかしは控えめに:スクロール中の再描画が重くなる
  • 枚数を絞る:10枚を超えるなら一覧ページへ誘導する
<article class="carousel__item">
  <img src="/img/case01.webp" alt="制作事例1" width="640" height="360" loading="lazy">
  <h3>事例タイトル</h3>
</article>

widthheightを属性で書いておくと、読み込み前から場所が確保されカクつきが消えます。地味ですが体感速度に効きます。

導入前に決めておく設計方針

実装は数行で終わるぶん、設計判断のほうが結果を左右します。着手前に次の4点を決めておくと、後戻りがありません。

1. 何枚見せるかを先に決める

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枚目に何を置くか

横スクロールは2枚目以降の閲覧率が大きく落ちます。最も伝えたい事例や、最も売りたいプランは必ず1枚目に置いてください。並び順を更新頻度で決めると、重要な情報が埋もれます。

3. 全件へ行く導線を用意する

カルーセルは「一部を見せる」ための形式です。10枚を超える場合は、末尾に一覧ページへのリンクカードを置くか、見出し横に「すべて見る」を添えます。

<article class="carousel__item -more">
  <a href="/works/">制作事例をすべて見る</a>
</article>

4. 計測して判断する

導入後は、2枚目以降が見られているかを確認します。スクロール到達率をイベントとして送れば、枚数や順番の判断材料になります。見られていないなら、枚数を減らすかグリッド表示に戻すのが正解です。

確認すること見る指標改善の方向
続きが見られているかスクロール到達率見切れ幅を増やす
カードが押されているかクリック率1枚目の内容を変える
離脱していないか直帰率枚数を減らす
表示が重くないかLCP・CLS遅延読み込みと比率固定

横スクロールは省スペースと引き換えに閲覧率を犠牲にする形式です。効果を測らずに置き続けるのではなく、数字を見て残すか戻すかを決めてください。


まとめ

scroll-snapは、スクロールする親にscroll-snap-type、止めたい子にscroll-snap-alignを書くだけで使えます。横カルーセルではカード幅を78%前後にして次の1枚を見切れさせ、flex: 0 0 幅で縮小を止めるのが要点です。端の余白はpaddingscroll-paddingを同じ値で揃え、勢いで飛ばしたくない場面ではscroll-snap-stop: alwaysを足します。縦方向はproximityを選び、PCではグリッドに戻す出し分けまで入れておけば、どの環境でも扱いやすいUIになります。

よくある質問(FAQ)

scroll-snapが全く効きません。

スクロールする親要素にoverflow-x: auto(縦ならoverflow-y)が指定されているか確認してください。スクロールが発生しない要素にはスナップも効きません。加えて、各子要素にscroll-snap-alignが必要です。親だけの指定では動きません。

mandatoryとproximityはどちらを使うべきですか?

カードを1枚ずつ確実に見せたい横カルーセルではmandatoryが適しています。縦方向のセクション送りではproximityを推奨します。mandatoryは必ず定位置へ吸い付くため、セクション内に長い文章があると途中で読めなくなります。

カードの幅が勝手に縮んでしまいます。

Flexboxの子要素は既定で縮むためです。flex: 0 0 78%のように縮小を0にして幅を固定してください。widthだけの指定では縮小が優先され、カードが潰れて並びます。

スクロールバーは消してもいいですか?

消す場合はカードを画面端で見切れさせ、横に続きがあると分かる形にしてください。バーも見切れもない状態では、スクロールできること自体に気づかれません。細く目立たない見た目に変えるだけの対応も有効です。

PCでも横スクロールのままで問題ありませんか?

マウスでの横スクロールは操作しにくいため、1024px以上ではグリッド表示に切り替えるのが親切です。display: gridscroll-snap-type: noneに戻すだけで実現できます。

あわせて読みたい関連記事

CSS・Webデザイン実装の総まとめ

CSSの実装テクニックを「レイアウト・装飾・アニメーション・基礎・トラブル解決」の目的別に探せる総まとめページを用意しています。実装で迷ったときの索引としてどうぞ。

AIスキルで、未来の自分をアップデート!

今なら完全無料でAIを学べる!WithAI

今なら完全無料でAIを学べる!

  • 動画や実践で楽しく学べる:初心者でも安心のカリキュラム
  • スマホ・PCどちらでもOK:好きな時間に学習できる
  • 料金は一切ナシ0円でAIスキルが身につく

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。

会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!

この記事を書いた人

WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次