CSSだけで、前後のボタンとマーカー付きのカルーセルを作る方法

​​2026年11月14日​CSS​CSS Scroll Snap,Playwright,アクセシビリティ,アンカー位置指定

CSS の ::scroll-button() と ::scroll-marker を使うと、横にスクロールするリストに「前へ」「次へ」のボタンと、何枚目を表示しているかを示すマーカー(丸い点)を、ブラウザーが付けてくれます。ボタンを押したときのスクロールや、端まで来たときにボタンを押せなくする処理、表示中のマーカーの切り替えは、ブラウザーが受け持ちます。JavaScript は1行も書きません。

5枚のスライドがあるカルーセル。「次へ」のボタンを3回押すと4枚目まで進み、そのたびに下のマーカーのピンクの点が右へ移る。最後に1つ目のマーカーを押すと1枚目に戻る
「次へ」を3回押してから、1つ目のマーカーで1枚目に戻る様子。画面上部の説明とカーソルの絵、スライドの色分けは、録画のために足したもの
補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。ページを Playwright で開いてクリックやキー操作をし、スクロール位置(scrollLeft)と、Chrome DevTools Protocol で取り出したアクセシビリティツリーを調べました。Firefox や Safari での表示、スクリーンリーダーでの読み上げは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

必要なのは、スクロールするリストと4つの擬似要素・擬似クラス

材料は、横にスクロールする要素と、その中のアイテムだけです。ここでは <ul> を使い、1つの <li> を1枚のスライドにします。data-label は、あとでマーカーの名前に使います。

html
<ul class="carousel">
  <li data-label="スライド 1">1</li>
  <li data-label="スライド 2">2</li>
  <li data-label="スライド 3">3</li>
  <li data-label="スライド 4">4</li>
  <li data-label="スライド 5">5</li>
</ul>

CSS は次のとおりです。検証環境では、このとおりに書いたページで、ボタンとマーカーのどちらを押してもスライドが切り替わりました。

css
/* 横にスクロールするリスト */
.carousel {
  width: 400px;
  height: 200px;
  margin: 40px auto 60px;
  padding: 0;
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
  anchor-name: --carousel;
  scroll-marker-group: after;
}
.carousel li {
  list-style: none;
  flex: 0 0 100%;
  scroll-snap-align: start;
  display: grid;
  place-items: center;
  font-size: 64px;
  background: #eefafd;
}

/* 前へ・次へのボタン */
.carousel::scroll-button(*) {
  position: absolute;
  position-anchor: --carousel;
  align-self: anchor-center;
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 50%;
  background: #00c8e8;
  color: #fff;
  cursor: pointer;
}
.carousel::scroll-button(*):disabled {
  opacity: 0.3;
  cursor: default;
}
.carousel::scroll-button(left) {
  content: "◀" / "前へ";
  right: calc(anchor(left) + 8px);
}
.carousel::scroll-button(right) {
  content: "▶" / "次へ";
  left: calc(anchor(right) + 8px);
}

/* マーカー */
.carousel::scroll-marker-group {
  position: absolute;
  position-anchor: --carousel;
  top: calc(anchor(bottom) + 12px);
  justify-self: anchor-center;
  display: flex;
  gap: 12px;
}
.carousel li::scroll-marker {
  content: attr(data-label);
  width: 16px;
  height: 16px;
  box-sizing: border-box;
  border: 2px solid #00c8e8;
  border-radius: 50%;
  background: #fff;
  overflow: hidden;
  text-indent: 16px;
}
.carousel li::scroll-marker:target-current {
  background: #d85878;
  border-color: #d85878;
}

使っている擬似要素と擬似クラスは、次の4つです。

書き方 何を表すか
::scroll-button(left) など スクロールコンテナーの中に作られる、スクロール用のボタン
::scroll-marker-group マーカーをまとめて並べる入れ物。scroll-marker-group プロパティを書いたときに作られる
::scroll-marker 各アイテムに1つずつ作られるマーカー
:target-current いま表示しているアイテムのマーカー

ボタンとマーカーの置き場所は、CSS のアンカー位置指定で決めています。リストに anchor-name: --carousel で名前を付け、ボタンとマーカーの入れ物を position-anchor: --carousel でリストに結び付けます。right: calc(anchor(left) + 8px) は「ボタンの右端を、リストの左端から 8px 外側に置く」という意味です。ボタンとマーカーをアンカー位置指定で置くという考え方は、MDN の CSS によるカルーセルの作成のガイドと同じです。

content を書かないと、ボタンもマーカーも作られない

::scroll-button() も ::scroll-marker も、content に none 以外の値を書いたときだけ作られます。マーカーのほうは、さらにスクロールコンテナーに scroll-marker-group を none 以外の値で書いておく必要があります。

検証環境で、検証用のカルーセルのページから ::scroll-button() と ::scroll-marker の content の行だけを消して開きました。アクセシビリティツリーにはボタンもタブも1つもなく、ふだんなら「次へ」のボタンが表示される位置をクリックしてもスクロールしませんでした。

::scroll-button() のかっこの中には、left・right・up・down のほか、inline-start・inline-end・block-start・block-end が書けます。* を書くと、すべてのボタンにまとめてスタイルを当てられます。仕様には next と prev もありますが、MDN にはどのブラウザーも対応していないと書かれています。

ボタンは1ページ分ずつ進み、端で押せなくなる

5枚のスライド(1枚の幅 400px)で「次へ」を押すと、scrollLeft は 0 → 400 → 800 → 1200 → 1600 と、1枚分ずつ増えました。1600 は最後のスライドの位置で、そこでもう一度「次へ」を押しても 1600 のままでした。最初の位置で「前へ」を押したときも、0 から動きませんでした。

端まで来たボタンは、ブラウザーが押せない状態(disabled)にします。アクセシビリティツリーでも、最初の位置では「前へ」のボタンが disabled=true になっていました。この状態は :disabled で選べるので、上の CSS では薄く表示しています。

MDN の ::scroll-button() のページには、ボタンを押すとスクロールコンテナーのおよそ1ページ分(コンテナーの大きさくらい)スクロールすると書かれています。そのため、1枚ずつきっちり止めたいときは、スクロールスナップ(scroll-snap-type と scroll-snap-align)を合わせて書きます。検証環境で、スクロールスナップの2行だけを消して「次へ」を押したところ、scrollLeft は 350 → 700 → 1050 と、幅 400px のコンテナーで 350px ずつ進みました。スライドの境目で止まらず、2枚のスライドがまたがって見えます。

1ページに複数のアイテムを並べた場合も、進むのは1ページ分です。li の flex を 0 0 33.333% にして9枚を3枚ずつ表示したところ、「次へ」1回で scrollLeft は 400 増え、3枚分進みました。

マーカーは、対応するアイテムへ移動するタブになる

::scroll-marker は、元になった要素へのリンクのように動きます。検証環境で1つ目から5つ目のマーカーを順に押すと、scrollLeft は 0、400、800、1200、1600 になり、押したマーカーのスライドが表示されました。

:target-current が付くのは、いま表示しているスライドのマーカーだけです。冒頭の GIF のとおり、「次へ」のボタンで進んだときも、ピンクの点が表示中のスライドのマーカーへ移ります。

マーカーの入れ物を after にしたのは、マーカーを見た目でもリストの後ろ(下)に置くためです。MDN の scroll-marker-group のページでは、before か after かでレイアウトの順番と Tab キーで移る順番が決まり、after ならどちらも最後になるとされています。見た目の位置と Tab の順番は合わせるのがよい、とも書かれています。ただし、検証環境での Tab の順番は、後のキーボードの節のとおり MDN の説明と違いました。

ボタンとマーカーには、読み上げ用の名前を付ける

生成されたボタンとマーカーは、支援技術からも見えます。検証環境でアクセシビリティツリーを調べたところ、ボタンは button、マーカーの入れ物は tablist、各マーカーは tab になっていました。表示中のスライドのマーカーだけが selected=true です。

名前は、content に書いた値で変わりました。

content の書き方 アクセシビリティツリーでの名前
content: "◀" / "前へ" 前へ
content: "◀" ◀
content: attr(data-label)(マーカー) スライド 1、スライド 2……
content: ""(マーカー) 空

/ のあとに書いた文字列は、content の代わりのテキストです。MDN の ::scroll-button() のページには、ブラウザーが自動で適切な名前を付けるので、代わりのテキストは要らないと書かれています。しかし検証環境の Chromium 153 では、代わりのテキストを書かないと、ボタンの名前は記号の「◀」のままでした。英語版の MDN のカルーセルのガイドでは、自動で名前を付けないブラウザーにも備えて、アイコンに代わりのテキストを付けるよう勧めています。

マーカーの content: "" は見た目のうえでは困りませんが、名前のないタブが5つ並ぶことになります。英語版の MDN のガイドには、空の名前は WCAG の 4.1.2(名前・役割・値)を満たさないので、文字の値を入れ、attr() で要素の data- 属性から名前を取る方法が紹介されています。上の CSS では、attr(data-label) で名前を入れ、text-indent と overflow: hidden で文字を丸の外へ追い出して隠しています。

キーボードでは、Tab でマーカーへ、矢印キーでスライドを切り替える

検証環境で2枚目を表示した状態から Tab キーを押していくと、フォーカスは次の順に移りました。フォーカスの位置は、検証用に :focus-visible で太い枠を付けた画面で確かめました。

  1. 2つ目のマーカー(表示中のスライドのマーカー)
  2. 「前へ」のボタン
  3. 「次へ」のボタン
  4. リスト(ul)全体

scroll-marker-group: after のままでも、マーカーがボタンより先にフォーカスされました。マーカーで止まるのは、表示中の1つだけでした。マーカーにフォーカスがある状態で → キーを押すと、フォーカスが隣のマーカーに移り、表示も3枚目、4枚目と切り替わりました。← キーで戻ります。MDN の ::scroll-marker のページにも、マーカーは tablist と tab として扱われ、左右のカーソルキーでページを移れると書かれています。

Firefox と Safari は、まだ対応していない

互換性データでの対応状況は次のとおりです。ここで使った ::scroll-button()・::scroll-marker・::scroll-marker-group・:target-current・scroll-marker-group は、どれも同じでした。MDN では、まだ実験的な機能とされています。

ブラウザー 対応
Chrome・Edge(Android の Chrome を含む) 135 から
Opera 120 から
Samsung Internet 29.0 から
Firefox(Android を含む) 対応していない
Safari(iOS を含む) 対応していない

検証環境の Chromium 153 では、CSS.supports('selector(::scroll-button(left))')、CSS.supports('selector(::scroll-marker)')、CSS.supports('scroll-marker-group', 'after') が、どれも true を返しました。JavaScript で対応しているかを調べたいときは、この書き方が使えます。

​Posted by 86