CSSのscroll-stateで、止まったカードを目立たせる方法
CSS の scroll-state クエリを使うと、横スクロールのリストでぴたっと止まったカードだけに、濃い枠や白い背景を付けられます。カルーセルやスライダーで「いま表示中のカード」を強調し、ほかのカードを薄く見せたい場面で使えます。スクロールの位置を JavaScript で調べて class を付け替える処理は要りません。カードに container-type: scroll-state を付け、@container scroll-state(snapped: x) の中にカードの中身のスタイルを書きます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、ヘッドレスの Chromium 153 です。幅360pxの画面で HTML と CSS のページを開き、指の操作を Chrome DevTools Protocol のタッチイベントで再現しました。どのカードが条件に一致しているかは、@container の中で付けたカスタムプロパティの値を読んで調べています。Firefox と Safari、スマートフォンの実機、トラックパッドやマウスホイールでの操作は試していません。

カードを scroll-state のコンテナーにして、中身にスタイルを付ける
HTML は、カードを <ul> に並べたものです。スタイルを付ける先として、カードの中に .card-body の <div> を1つ入れておきます。
<section aria-label="新着の記事">
<ul class="card-list">
<li class="card"><div class="card-body"><h3>カード 1</h3><p>止まっているカードだけ、枠が濃くなります。</p></div></li>
<li class="card"><div class="card-body"><h3>カード 2</h3><p>ほかのカードは少し小さく、薄く表示されます。</p></div></li>
<!-- カード 3〜6 も同じ形 -->
</ul>
</section>
CSS は次のとおりです。リストに scroll-snap-type、カードに scroll-snap-align を付けてカードの左端で止まるようにし、さらにカードに container-type: scroll-state を付けます。
/* 横スクロールで、カードの左端に止まるリスト */
.card-list {
display: flex;
gap: 12px;
margin: 0;
padding: 8px 16px 16px;
list-style: none;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 16px;
}
.card {
flex: 0 0 75%;
scroll-snap-align: start;
container-type: scroll-state;
}
/* 見た目はカードの中の要素に付ける */
.card-body {
box-sizing: border-box;
height: 100%;
padding: 16px;
border: 2px solid #b8e6ef;
border-radius: 12px;
background: #eefafd;
transition: background-color 0.3s, border-color 0.3s, opacity 0.3s, scale 0.3s;
}
/* scroll-state クエリが使えるブラウザーだけ、止まっていないカードを薄くする */
@supports (container-type: scroll-state) {
.card-body {
opacity: 0.5;
scale: 0.94;
}
}
/* 止まっているカードだけを目立たせる */
@container scroll-state(snapped: x) {
.card-body {
opacity: 1;
scale: 1;
border-color: #00c8e8;
background: #ffffff;
}
}
snapped: x は、横方向のスクロールでいま止まっている(または、これから止まろうとしている)カードに一致します。MDN のスクロール状態クエリーの解説のとおり、@container の中のルールが付くのは、条件を満たしたコンテナーの子孫です。
検証環境で開いた直後は、1枚目のカードだけが一致しました。scrollTo() でリストの右端まで送ると、6枚目のカードが一致しました。最後のカードは左端がそろう位置までスクロールできませんが、右端で止まったときはそのカードが一致します。
カード自身には、スタイルが付かない
@container の中に .card { ... } と書いても、効きません。検証環境で、カードを4枚並べた簡単なページを作り、@container scroll-state(snapped: x) の中に outline を書いて比べました。
@container の中に書いたセレクター |
1〜4枚目に outline が付いたか |
|---|---|
.card(コンテナー自身) |
どれにも付かない |
カードの中の <div>(記事の CSS の .card-body にあたる要素) |
1枚目だけに付く |
カードそのものの枠や背景を変えたいときも、上の CSS のように中に1つ要素を入れて、その要素に見た目を付けます。MDN の例でも、<section> の中にスタイル用の .wrapper の <div> を入れています。
強調は、指を離す前に切り替わる
GIF の2回目と3回目のように、カードを長めに動かすと、指を離す前に次のカードが目立ちます。検証環境で指を 5px ずつ左へ動かすと、scrollLeft が 130px になったところで、一致するカードが1枚目から2枚目に変わりました。カードの幅と間隔を足した 258px の、ほぼ半分です。そのまま指を戻すと、125px で1枚目に戻りました。
CSS Conditional Rules Level 5 の仕様は、snapped を「スナップターゲットが、スナップしている、またはスナップしようとしているか」を調べるものとし、scrollsnapchanging イベントが発生する対象に一致する、と書いています。
MDN の scrollsnapchanging イベントのページによると、このイベントは、いまのスクロールの操作が終わったときに止まる先が新しいカードに変わったと、ブラウザーが判断した時点で発生します。指で動かしている途中にも発生します。
一方、MDN(英語版)の scrollsnapchange イベントのページによると、scrollsnapchange は、スクロールの操作が終わり、新しい止まる先が決まったときに発生します。
指を左へ 160px 動かして離したときの、イベントの順番は次のとおりでした(時刻はページを開いてからの経過)。
2884ms scrollsnapchanging -> カード 2
3043ms 指を離す
3300ms scrollsnapchange -> カード 2
一致するカードは、指を離す前に2枚目に変わっていました。止まる先を2枚目とする scrollsnapchanging が発生したのも、指を離す前です。仕様のとおり、snapped は止まる先が変わった時点で一致するので、カードが止まり切るのを待ってから強調する、という動きにはなりません。
キーボードでも同じように切り替わります。リストにフォーカスを置いて右矢印キーを押すと、1回ごとに1枚ずつ送られ、一致するカードも2枚目、3枚目、4枚目と変わりました。
snapped に書ける値と、横スクロールでの一致
MDN の @container のページには、snapped の値として none、x、y、block、inline、both が挙がっています。「カード自身には、スタイルが付かない」で使ったのと同じ、カードを4枚並べた横スクロールのページで、値を変えて1〜4枚目のどれに一致するかを調べました。
| 書いた条件 | 一致したカード |
|---|---|
snapped: x |
1枚目 |
snapped: inline |
1枚目 |
snapped: y |
なし |
snapped: block |
なし |
snapped: both |
なし |
snapped: none |
2〜4枚目 |
横書きのページの横スクロールなら、x と inline は同じ結果です。both は、縦と横の両方で止まる対象になっているときにだけ一致します。
snapped: none は、止まっていないカードに一致しました。止まっていないカードのほうに薄くするスタイルを書きたいときは、@container scroll-state(snapped: none) の中に書く形も使えます。
スタイルが付かないときに見直すところ
次のどれかに当てはまると、どのカードにもスタイルが付きませんでした。
- カードに
container-type: scroll-stateがない(何も書かないときも、inline-sizeにしたときも一致しない) - カードに
scroll-snap-alignがない @containerの中で、カード自身にスタイルを書いている
container-type を付けていないときは、not scroll-state(snapped: x) のように否定にしても、どのカードにも一致しませんでした。
MDN の container-type のページによると、scroll-state はサイズのクエリと違い、要素のサイズの計算に影響する抑制(containment)をかけません。カードの幅に合わせたコンテナークエリも使いたいときは、container-type: inline-size scroll-state; のように2つ並べて書けます。検証環境でも、2つ並べたときに snapped: x が1枚目に一致しました。
対応していないブラウザーへの備え
@mdn/browser-compat-data 8.1.8 の互換性データでは、scroll-state クエリの snapped に対応しているのは Chrome と Edge の 133 以降で、Firefox と Safari は対応していません。実験的な機能(experimental)の扱いです。
そのため上の CSS では、止まっていないカードを薄くするスタイルを @supports (container-type: scroll-state) の中に入れています。MDN の @supports のページのとおり、@supports (container-type: scroll-state) は、ブラウザーがこの宣言に対応しているかどうかを調べます。条件が成り立てば、中の CSS が使われます。検証環境の Chromium では、CSS.supports('container-type', 'scroll-state') が true でした。





