CSSで横スクロールのカードをぴったり止めるには
横に並べたカードの親に scroll-snap-type を、各カードに scroll-snap-align を指定し、スクロールが止まったときにカードの左端がそろう位置へ合わせる。
JavaScript を使わずに、カードごとに止まるカルーセルの動きを作れる。
静的確認は、CSS の構文と仕様で確認し、ブラウザでの描画確認までは行っていないものである。
公開:
<div class="carousel" tabindex="0" role="region" aria-label="カードの一覧(横にスクロールできる)">
<div class="slide">
<span class="slide__title">カード 1</span>
<span class="slide__text">横にスクロールすると、カードの左端がそろう位置で止まる。</span>
</div>
<div class="slide">
<span class="slide__title">カード 2</span>
<span class="slide__text">カードの途中の半端な位置では止まらない。</span>
</div>
<div class="slide">
<span class="slide__title">カード 3</span>
<span class="slide__text">右端に次のカードが少し見えている。</span>
</div>
<div class="slide">
<span class="slide__title">カード 4</span>
<span class="slide__text">止まったカードの左にも 16px の余白が残る。</span>
</div>
<div class="slide">
<span class="slide__title">カード 5</span>
<span class="slide__text">これが最後のカードになる。</span>
</div>
</div>/* 横に並べてはみ出させ、はみ出した分をスクロールさせる */
.carousel {
display: flex;
gap: 12px;
max-width: 360px;
overflow-x: auto;
padding: 4px 16px 12px;
/* 止まる位置を持たせるのはスクロールする側(親) */
scroll-snap-type: x mandatory;
/* padding と同じ値にして、止まったカードの左にも余白を残す */
scroll-padding-inline: 16px;
}
.slide {
/* 縮ませない。縮むと全部が枠に収まり、スクロールが起きない */
flex: 0 0 75%;
/* どこをそろえて止めるかは子に書く */
scroll-snap-align: start;
box-sizing: border-box;
height: 110px;
padding: 16px;
border-radius: 12px;
background: #ccfbf1;
color: #134e4a;
}
.slide:nth-child(even) {
background: #e0e7ff;
color: #312e81;
}
.slide__title {
display: block;
margin-bottom: 6px;
font-weight: 600;
}
.slide__text {
font-size: 13px;
}親に scroll-snap-type: x mandatory を、カードに scroll-snap-align: start を書くと、スクロールが止まったときに必ずどれかのカードの左端が枠の左端にそろう。
カードの幅を 75% にしているのは、次のカードの端を見せて、横にスクロールできることに気付かせるためである。
つまずき
止まらないときは、scroll-snap-type をカードの側に書いていないかを確認するとよい。
scroll-snap-type はスクロールする要素(スクロールコンテナ)に書くプロパティで、子に書いても何も起きない。
逆に、どこをそろえるかを決める scroll-snap-align は子に書く。
もう 1 つの典型は、そもそもスクロールが起きていないケースである。
flex アイテムは既定で flex-shrink: 1 なので、幅を指定しても親の幅に収まるまで縮み、はみ出しが生まれない。
上の例では flex: 0 0 75% で縮まないようにしている。
親に overflow-x: auto が無い場合も、はみ出したカードは枠の外に描かれるだけで親はスクロールコンテナにならず、止まる位置も働かない。
mandatory と proximity の違い
scroll-snap-type の 2 つ目の値は、どれだけ強く止めるかを決める。
mandatory は、スクロールが止まったときに必ずどれかの止まる位置へ合わせる。
proximity は、合わせるかどうかをブラウザの判断に任せ、止まる位置から離れたところで手を離せばそのまま残ることがある。
scroll-snap-type: x のように軸だけを書くと proximity になるので、1 枚ずつ見せたいカードの一覧で「たまにしか止まらない」と感じたら、mandatory を書き忘れていないかを見るとよい。
padding だけでは止まる位置が端に寄る
親に padding を付けて左右に余白を作っても、止まる位置の基準はスクロールする領域の端のままである。
そのため scroll-padding を書かないと、止まったカードは余白を無視して枠の左端にぴったり寄る。
Chrome では、読み込んだ直後からスクロール位置が最初のカードに合わせて 16px 進んだ状態になり、付けたはずの左の余白が最初から見えない。
上の例のように scroll-padding-inline に padding と同じ値を書くと、止まる基準がその分だけ内側に寄り、どのカードで止まっても左に同じ余白が残る。
勢いよく送ったときに何枚も飛ばさない
mandatory でも、勢いよくスワイプすると途中の止まる位置を通り過ぎ、何枚も先のカードで止まることがある。
1 枚ずつ確実に送りたいときは、カードに scroll-snap-stop: always を指定すると、スクロールがそのカードの止まる位置を通り過ぎなくなる。
ただし、枚数の多い一覧を素早く流し見たい利用者には 1 枚ずつしか進まないことが煩わしくなるので、既定値の normal のままにするかどうかは一覧の長さで決めるとよい。
キーボードでも送れるようにする
横にスクロールする領域は、マウスのホイールやタッチなら操作できても、キーボードだけでは中に入れないことがある。
上の例で tabindex="0" を付けているのはそのためで、タブキーで領域に入ってから矢印キーで横に動かせる。
フォーカスできる要素にしたら、何の領域なのかが読み上げで分かるように role="region" と aria-label も添える。