スクロールスナップ
縦スクロールで全画面セクションがピタッと止まる。scroll-snap-type: y でページめくり感を演出。
プレビュー
操作枠内をスクロールすると動きます。
仕様書
- 向く場面
- フルスクリーンの自己紹介・作品ページ
- 区切りのはっきりした塊を1つずつ見せる
- オンボーディングの縦送り
- 避ける場面
- 連続した文章の記事(読む途中で吸着される)
- セクションが画面より高い内容
- 構成要素
- スナップ容器
- 全画面セクション
- スクロールヒント
- 持っている状態
- 通常
- キーボード
Tab でスナップ容器へ(tabindex=0)、↑↓/PageDown/Space でスクロール(吸着はネイティブ)
- 支援技術
ネイティブスクロールのまま吸着するので既定のキー操作が効く。セクション名は文字で表示。reduced-motion では scroll-snap-type: none にして強制吸着(めまいの原因)を外す
- 動き
なし(吸着の減速はブラウザのネイティブ挙動。CSS のアニメ指定は無い)
デザインの要点
- JS ゼロ。scroll-snap-type: y mandatory(容器)と scroll-snap-align: start(各セクション)だけで、慣性も減速もブラウザ任せ
- セクションの高さは容器と同じ 300px に揃える。容器より高いと読み終わる前に次へ吸い寄せられる
- mandatory は必ずどこかで止まる指定。記事のように途中で止まりたい内容は proximity にする
- overflow の div は既定でフォーカスできない。tabindex=0 を付けないとキーボードだけの利用者がスクロールできない
使いどころ
フルスクリーンの自己紹介ページ、写真ギャラリー、オンボーディングのような『区切りのはっきりした塊を1つずつ見せたい』構成に向く。セクションごとに情報が完結していて、途中で止まっても意味が通る内容と相性がよい。逆に、通常の記事やブログのような連続した文章には向かない。scroll-snap-type: y mandatory は必ずどこかのスナップ位置へ強制的に吸着させるため、セクションの途中でじっくり読みたい・行き来したいコンテンツをここに入れると、読もうとするたびに勝手に次のセクションへ引っ張られて読みにくくなる。セクションの高さがビューポートより高い場合も同様に、スクロールが完了する前に次のスナップへ吸い寄せられて中身が読めなくなることがある。
仕組み
この実装にJavaScriptは一切無く、CSSの scroll-snap-type: y mandatory(コンテナ側)と scroll-snap-align: start(各セクション側)だけで完結している。JSで位置を計算してscrollToするアプローチと違い、実際のネイティブスクロール(ホイール・トラックパッド・タッチのフリック・キーボード)をブラウザがそのまま扱い、指を離した/ホイールを止めた瞬間に最寄りのスナップ位置へ滑らかに減速して止まる物理演算もブラウザ任せになる。mandatory は『必ずどこかのスナップ点で止まる』指定で、緩めたい場合は proximity にすると、スクロール量が小さいときはスナップせず自由に止まれるようになる。-webkit-overflow-scrolling: touch はiOSでの慣性スクロールを有効にするための指定で、無いとタッチでの吸着挙動が硬くなることがある。
実装の注意
セクションの中に、それ自体がスクロールする長いリストや折りたたみパネルを置くと、親コンテナが先にスナップで次のセクションへ持っていかれてしまい、中の要素までホイールスクロールで届かないことがある。ネストしたスクロール領域を作る場合は、この構造を避けるかスナップの対象から外す必要がある。もう1つは、勢いよくスクロールしたときに1つのセクションを飛び越えて次のスナップ点まで一気に進んでしまう挙動で、途中のセクションを絶対に見せたい場合は scroll-snap-stop: always をそのセクションに指定して素通りを防ぐ(このデモには入れていない)。また .snap-root 自体がビューポートではなく高さ300pxの内側スクロール領域なので、ページ全体をスナップさせたい場合は html/body 側に指定を移し、固定ヘッダーの高さぶん scroll-padding-top で補正する必要がある。
アクセシビリティ
ネイティブのスクロールをそのまま使っているため、PageDown/PageUp・Space・矢印キーでのキーボードスクロールもブラウザの既定動作としてそのまま効く。ただし overflow-y: scroll を付けた div は既定ではフォーカス可能な要素ではないため、マウスやタッチを使わずキーボードだけで操作するユーザーのために .snap-root に tabindex="0" を付け、Tabで到達できるようにしておく必要がある(付けないと、内部にリンクなどフォーカス可能な要素が無い限りキーボードでスクロールする手段が無くなる。このデモでも付けている)。mandatory による強制的な吸着とスクロールの急な減速は、前庭障害があるユーザーには不快感やめまいの原因になりうるため、@media (prefers-reduced-motion: reduce) では scroll-snap-type: none に切り替えて自由なスクロールに戻す(このデモでもそうしている)。セクション同士は見出し(h2等)で区切っておくと、スクリーンリーダー利用者は見出しジャンプでスナップに関係なく移動できる。
調整して使う
軸をx方向にすればカルーセル的な横送りにもなり(実際、上のカルーセルもscroll-snap-type: xを利用している)、scroll-snap-align を start/center/end のどれにするかでセクションの合わせ位置が変わる。mandatory と proximity の使い分けが最大の調整点で、ストーリー性を強く出したいランディングページは mandatory、通常のセクション区切りに軽く添えたい場合は proximity が扱いやすい。ドットインジケーターやセクション番号を横に添えると、今どのセクションにいるか・あと何個あるかが伝わりやすくなる(この実装には無いが、IntersectionObserverと組み合わせて追加できる)。
コード(コピーして使えます)
<div class="snap-root" tabindex="0">
<section class="snap-sec" style="background:#2b50c7">Section 1</section>
<section class="snap-sec" style="background:#1a8c5b">Section 2</section>
<section class="snap-sec" style="background:#b5341e">Section 3</section>
</div>
<div class="snap-hint">↓ スクロール</div>