スクロールに絡む機能の実装
こんにちは! サイボウズ フロントエンドエキスパートチームの麦島です。今回のテーマは
ブラウザ上でスクロールに絡む機能を実装しようとすると、考慮すべき点や制約は多くなります。
たとえば、スクロールに応じて何か処理をする場合、JavaScriptによるスクロールの監視が必要です。このスクロールの監視処理は、イベントがトリガーされる回数が非常に多くなりやすく、パフォーマンスへの考慮が必要となります。
他にも、スクロールに応じた計算処理も複雑になりがちです。スクロールコンテナ自体のサイズ・
スクロール関連機能の拡充
実装時に考慮事項が多いスクロールですが、昨今では、スクロールに関連する仕様策定およびブラウザでの機能実装が進んでいます。
たとえば、Web Platform Statusでは、機能ごとのブラウザ互換性を表す指標であるBaselineの状況を確認できます。ここで"scroll"
で検索すると、多くの新しい仕様が存在し、一部はすでにブラウザ上でも実装済みであることがわかります。

これらの仕様には、従来課題とされていた点が大きく改善されるものも含まれます。それぞれ新しく何ができるようになり、どういった課題を解決するのかを確認していきましょう。
なお、今回紹介する機能は、本記事掲載時点では仕様的にはWorking Draftのものが含まれます。今後内容が変更される可能性がある点に注意してください。
スクロールバーのスタイリング
従来、スクロールバー自体のスタイルのカスタマイズには制限がありました。一部ブラウザでは::-webkit-scrollbar
擬似要素などでスタイル指定ができましたが、ブラウザが限定されます。
新しい仕様であるscrollbar-width
、scrollbar-color
、scrollbar-gutter
を用いることで、スクロールバー自体のスタイルおよび、スクロールバーの表示有無に伴うレイアウトの調整が可能となりました。
scrollbar-width
, scrollbar-color
scrollbar-width
は、スクロールバーの幅を指定するためのプロパティです。
デフォルトであるauto
に加えて、次の値が指定できます。
none
: スクロールバーを非表示にする(スクロールは可能) thin
: デフォルトより細いスクロールバーを表示する
scrollbar-color
は、スクロールバーのカラーを変更するためのプロパティです。カラーは2色指定可能で、1つ目はスクロールバーのノブ
scrollbar-width
およびscrollbar-color
の両方を指定した例を見てみましょう。
<div class="container">
<div class="content"></div>
</div>
.container {
overflow-y: scroll;
height: 150px;
width: 150px;
border: 1px solid #000;
/* スクロールバーの幅を細く */
scrollbar-width: thin;
/* つまみを黒、背景を赤に */
scrollbar-color: black red;
}
.content {
height: 1000px;
}
同様のスタイルでスクロールバーのスタイルだけを含めないコンテナと並べた場合、次のような結果となります。

スクロールバーの幅がやや細くなり、背景が赤く、つまみが黒色になりました。
なお、見た目の変更はアクセシビリティにも影響を与える点には注意が必要です。例えばカラーを変更する場合には、適切なコントラストを確保する必要があり、これは仕様上でも次のように言及されています[1]。
scrollbar-gutter
scrollbar-gutter
は、スクロールバーの幅分の余白を確保するためのプロパティです。
従来のよくある問題として、スクロールバーが自動的に表示される際に、スクロールバーの幅だけレイアウトが変化してしまう、というものが挙げられます。
次の例を見てみましょう。
<div class="container-wrapper">
<div class="container">
<!-- 折り返すコンテンツ -->
<div class="content">ABCDEFGHIJKLMNOPQRSTUVWXYZ</div>
</div>
<!-- スクロールが発生するコンテナ -->
<div class="container">
<!-- 複数の折り返すコンテンツ -->
<div class="content">ABCDEFGHIJKLMNOPQRSTUVWXYZ</div>
<div class="content">ABCDEFGHIJKLMNOPQRSTUVWXYZ</div>
</div>
</div>
.container-wrapper {
display: flex;
gap: 16px;
}
.container {
overflow-y: auto;
height: 150px;
width: 150px;
border: 1px solid black;
}
.content {
word-break: break-all;
border: 1px solid red;
padding: 4px;
margin: 4px;
}
それぞれ、2つあるコンテナは横幅に入り切らずに折り返すコンテンツを保持しています。また、後者のコンテナは、より多くのコンテンツを含め、スクロールが発生するようにしています。
この場合、次のような見た目となります。

ポイントは、内部のテキストの折り返し位置が変わっているという点です。
これは、スクロールバーが表示されると、その幅に応じてコンテンツの表示可能領域も変化するのが原因です。
動的にデータを取得してコンテンツを追加していくようなケースでは特に目立ちやすく、スクロールバーが表示されたタイミングで表示がガタついたような印象を受けます。
scrollbar-gutter
を指定し、あらかじめスクロールバーの幅分の余白を確保しておくことで、このガタつきを抑制できます。
先程の例に、scrollbar-gutter
だけを新たに追加してみます。
.container {
overflow-y: auto;
height: 150px;
width: 150px;
border: 1px solid black;
/* スクロールバーの余白を確保 */
scrollbar-gutter: stable;
}
すると、次のようにスクロールバーの表示有無に関わらず、スクロールバーの幅分だけ余白が確保されており、コンテンツの折り返し位置が統一されていることがわかります。
scrollbar-gutter
を指定しスクロールバーの余白を確保した例
なお、上記のstable
を指定した例では、右側にのみ余白が確保されています。結果的にコンテンツが左に寄っている印象を受けますが、値としてstable both-edges
を指定することで、左側にも同様の余白を確保し、コンテンツが中央に来るように表示することも可能です。
CSSによるスクロールのコントロール
スクロールは、スクロールバーのドラッグ・
しかし、提供するアプリケーションによっては、次のように任意のタイミングでのスクロールが必要となるケースがあります。
- ボタンをクリックした際にスクロールしてほしい
- 複数並んでいるコンテンツのうち、次
(前) の要素までスクロールさせたい - 指定のコンテンツの位置までスクロールさせたい
従来これらはJavaScriptによる制御が必須でしたが、新しいCSS仕様を用いることで、CSSのみで基本的なスクロールのコントロールを実現できます。
::scroll-button()
擬似要素
::scroll-button()
擬似要素は、スクロール可能なコンテナ単位で、スクロールを制御するためのボタンを表すための擬似要素です。
通常は非表示ですが、CSS上でcontent
にnone
以外の有効な値を指定すると表示されます。
まずは、ミニマムな例で動作を見てみましょう。
<div class="container">
<div class="content">
<div>先頭コンテンツ</div>
<div>中央コンテンツ</div>
<div>末尾コンテンツ</div>
</div>
</div>
.container {
overflow-y: scroll;
height: 150px;
width: 150px;
border: 1px solid black;
/** 上方向へのスクロールボタンを表示 */
&::scroll-button(up) {
content: "▲";
}
/** 下方向へのスクロールボタンを表示 */
&::scroll-button(down) {
content: "▼";
}
}
.content {
height: 500px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ポイントは、&::scroll-button(up)
および&::scroll-button(down)
の箇所です。content
以外のプロパティは特に何も指定していません。
動作を確認してみると、次のようにcontent
に指定した文字列でボタンが表示されており、up
およびdown
に対応したボタンをクリックすると、該当する方向にスクロールします。
::scroll-button()
の基本的な動作例(アニメーションGIF)
JavaScriptでの実装は一切無い状態で、スクロールボタンが実現できました。
up
down
以外にもleft
right
や、論理プロパティに相当するblock-start
block-end
inline-start
inline-end
なども指定可能です。また、*
を指定することで、共通したボタンのスタイリングも行えます。
/** スクロールボタンのスタイリング */
&::scroll-button(*) {
background-color: #000;
color: #fff;
border-radius: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
margin: 4px;
}
::scroll-button()
の共通スタイリングを指定した例
::scroll-marker
および::scroll-marker-group
擬似要素
::scroll-button()
は、単純に方向を指定したスクロール用のコントロールを提供します。一方で、
代表的な例がカルーセルです。
カルーセルは、写真やバナーなどを横スクロールで表示し、コンテンツ付近にあるマーカーをクリックすることでスクロールして対応するコンテンツが中央に来る、といったもので、ライブラリの利用や独自実装などで広く利用されています。
::scroll-marker
と::scroll-marker-group
擬似要素を用いることで、クリックすることでスクロールするマーカーの表示および、マーカーを束ねた位置などのスタイリングが可能となります。
多少わかりづらいですが、重要なのは次の点です。
- マーカーに対応する要素配下の
::scroll-marker
擬似要素でマーカーをスタイリングする - スクロール可能なコンテナに
scroll-marker-group
でマーカーの表示位置を決める - スクロール可能なコンテナ配下の
::scroll-marker-group
擬似要素でマーカーを束ねた配置・整列などのスタイリングを行う
実際に見てみましょう。
まず、シンプルに横スクロール可能なコンテナを作ってみます。
<div class="container">
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
<div class="content">4</div>
<div class="content">5</div>
<div class="content">6</div>
<div class="content">7</div>
</div>
.container {
overflow-x: scroll;
width: 200px;
height: 100px;
border: 1px solid black;
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
}
.content {
width: 50px;
height: 50px;
min-width: 50px;
min-height: 50px;
border: 2px solid red;
display: inline-flex;
justify-content: center;
align-items: center;
}

横スクロール可能なコンテナができました。
では、ここに::scroll-marker
および::scroll-marker-group
擬似要素でマーカーを表示してみましょう。
CSSを次のように変更します。
.container {
/* 〜省略〜 */
/* マーカーグループの表示位置を指定 */
scroll-marker-group: after;
/* マーカーグループのスタイリング */
&::scroll-marker-group {
/* マーカーを横並びに配置 */
display: flex;
gap: 8px;
margin-top: 8px;
}
}
.content {
/* 〜省略〜 */
/* マーカーのスタイリング */
&::scroll-marker {
display: block;
content: "";
width: 16px;
height: 16px;
border: 1px solid red;
border-radius: 100%;
}
/* 現在のマーカーのスタイリング */
&::scroll-marker:target-current {
border: 1px solid red;
background-color: red;
}
}
::scroll-marker
の動作例(アニメーションGIF)
JavaScriptに頼ることなく次の挙動が実現できました。
- コンテンツに対応するマーカーを表示
- マーカーをクリックすると、対応する要素の位置までスクロール
- スクロール位置に相当するマーカーのスタイルを切り替える
一方で、従来であれば、JavaScriptで次のような実装が必要でした。
- 各マーカー相当のボタンに対してクリックイベントを設定する
- ボタンがクリックされたら、対応する要素を取得し、該当する位置までスクロールさせる
- スクロール自体も監視し、スクロール量に応じてマーカーのclassなどを着脱しスタイルを切り替える
::scroll-marker
や::scroll-marker-group
によって、いかに実装が軽減できるかがわかるかと思います。
Scroll Snap Events
CSSのスクロールには、Scroll Snapと呼ばれる機能が存在します。Scroll Snapは、ユーザーによるスクロールが停止したタイミングで、指定した要素の境界や特定の位置で自動的にスクロールを止める
Scroll Snapはscroll-snap-type
やscroll-snap-align
プロパティの指定で有効化できますが、これ自体は新しいプロパティではありません。
次のイメージは、Scroll Snapの動作例です。各要素の中央を基準としており、スクロールバーのドラッグを解除した瞬間、近い要素が中央に来るように自動的にスクロール位置が補正されています。

従来では、Scroll Snapでスナップされた要素を取得するためには、Intersection Observer APIを用いて監視するといった工夫が必要でした。
Scroll Snapに関するECMAScriptの新しい仕様であるSnap Eventsでは、スナップされる要素の切り替わりに応じて"scrollsnapchange"
イベントおよび"scrollsnapchanging"
イベントがトリガーされ、スナップされている要素を簡単に特定できるようになります。
「スナップされた要素に応じた説明文を表示したい」
まず、簡単なスクロールスナップを実現するHTMLとCSSを用意します。
<section class="container">
<article class="content" data-index="0">
<h4>CSS Modern Features no.1</h4>
</article>
<article class="content" data-index="1">
<h4>CSS Modern Features no.2</h4>
</article>
<article class="content" data-index="2">
<h4>CSS Modern Features no.3</h4>
</article>
<article class="content" data-index="3">
<h4>CSS Modern Features no.4</h4>
</article>
<article class="content" data-index="4">
<h4>CSS Modern Features no.5</h4>
</article>
<article class="content" data-index="5">
<h4>CSS Modern Features no.6</h4>
</article>
</section>
<div class="subtitle"></div>
/* 見た目の調整用のスタイルは省略 */
.container {
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.content {
scroll-snap-align: center;
}
これまでの本連載タイトルの一部が列挙されています。
しかし、.subtitle
を持つ<div>
要素については空となっており、ここにスクロールスナップされた項目に相当するサブタイトルを表示したいと思います。
これは、次のようなJavaScriptを用意することで実現できます。
const SUB_TITLES = [
"新しい擬似クラス:has()、:is()、:where()を使いこなそう",
"Container Queries/祖先要素に応じたCSSの切り替え",
"Cascade Layers/レイヤーによる優先順位の制御",
"CSS Nesting Module/CSSの入れ子指定",
"Scoped Styles/スコープ付きスタイルルール",
"Cascade Sorting Order/CSSの適用順序を学び直す",
];
document.addEventListener("DOMContentLoaded", () => {
const container = document.querySelector(".container");
const subtitle = document.querySelector(".subtitle");
// 初期状態のタイトルを表示
subtitle.innerHTML = SUB_TITLES[0];
// スナップ要素が変更された際のイベント
container.addEventListener("scrollsnapchange", (event) => {
// スナップ要素の data-index 属性をもとにサブタイトルを取得
const index = Number(event.snapTargetBlock.dataset.index);
subtitle.innerHTML = SUB_TITLES[index];
});
});
"scrollsnapchange"
イベントによりスクロールスナップの変更を監視し、SnapEventに含まれるsnapTargetBlock
をもとに、スナップされた要素を取得しています。
実際の動作は次のようになります。

スクロールすると要素の中央にスナップされ、該当する要素に対応するサブタイトルが表示されています。
このように、Scroll Snap Eventsを用いることで、スナップ要素の特定が非常に簡単に行えます。
Container Scroll-State Queries
本連載内のCSS Modern Features no.
該当の記事の中では、Container Queriesの中でも、主に次の2つの機能を取り上げました。
- Container Size Queries: コンテナのサイズを判定に用いてスタイルを切り替える
- Container Style Queries: コンテナが特定のスタイルを持つかを判定に用いてスタイルを切り替える
そして、Container Queriesにはもう一種類の仕様が存在し、それが今回紹介するContainer Scroll-State Queriesです。Container Scroll-State Queriesでは、コンテナのスクロール状態を判定に用いてスタイルの切り替えが可能です。
簡単な例を見てみましょう。先述の::scroll-button()
擬似要素による上方向へのスクロールボタンを、スクロール可能な場合のみ表示するようにしてみます。
<div class="container">
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
</div>
/* 見た目の調整用のスタイルは省略 */
.container {
overflow-y: scroll;
/* コンテナとして定義 */
container-type: scroll-state;
/* スクロールボタンを非表示状態で有効化 */
&::scroll-button(up) {
content: "▲";
opacity: 0;
transition: opacity 0.2s;
}
/* 上方向へスクロール可能な場合のみ有効となるスタイル */
@container scroll-state(scrollable: top) {
/* スクロールボタンを表示 */
&::scroll-button(up) {
opacity: 1;
}
}
}
@container scroll-state(scrollable: top)
によって、コンテナが上方向へスクロール可能な場合のみのスタイルを宣言している点がポイントです。
実際の動作は次のようになります。

下方向へ少しスクロールし、上方向へスクロール可能な状態になると、::scroll-button(up)
によるスクロールボタンが表示されています。
なお、例ではscroll-state
に対してscrollable
を指定していますが、実際には次の3種類を指定可能です。
scrollable
snapped
stuck
それぞれどういった違いがあるのか確認してみましょう。
scrollable
先述の例でも用いたscrollable
は、単純に
@container scroll-state(scrollable: top) {
.target {
/* 上方向へスクロール可能な場合のみのスタイル */
display: block;
}
}
スクロール方向を示す値には、次のものが指定可能です。
none
: スクロール不可の場合top
: 上方向へスクロール可能な場合bottom
: 下方向へスクロール可能な場合right
: 右方向へスクロール可能な場合left
: 左方向へスクロール可能な場合y
: 上下いずれかの方向へスクロール可能な場合x
: 左右いずれかの方向へスクロール可能な場合
また、論理プロパティに相当するblock
block-start
block-end
inline
inline-start
inline-end
も利用可能です。
scrollable
を用いると、JavaScriptでスクロール量を計算することなく、スクロール可否によってスタイルを切り替えることができます。
snapped
snapped
を指定すると、Scroll Snapによって要素がスナップされているかをもとに判定を行います。性質上、Scroll Snapが有効であることが前提となります。
代表的なユースケースは、スナップされた要素のスタイル変更でしょう。
<div class="container">
<div class="content">
<div class="title">1</div>
</div>
<div class="content">
<div class="title">2</div>
</div>
<div class="content">
<div class="title">3</div>
</div>
<div class="content">
<div class="title">4</div>
</div>
<div class="content">
<div class="title">5</div>
</div>
</div>
/* 見た目の調整用のスタイルは省略 */
.container {
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.content {
scroll-snap-align: center;
/* スナップ対象をコンテナとして定義 */
container-type: scroll-state;
/* 非スナップ時のスタイル */
.title {
font-size: 1em;
transition: font-size 0.5s;
}
/* スナップ時のスタイル */
@container scroll-state(snapped: y) {
.title {
font-size: 2em;
font-weight: bold;
}
}
}
snapped
の利用時は、次の点がポイントとなります。
- スクロールコンテナではなく、スナップ対象をコンテナとして定義する
- スナップ対象の子要素に対してスタイルを適用する
上記例の場合は、スナップ対象の.content
要素をコンテナとして定義し、その子要素である.title
要素に対してスタイルを適用しています。
動作例は次のとおりです。

スナップされた要素のみ、文字のスタイルが切り替わっています。
JavaScriptでScroll Snap Eventsを用いても同様の制御は行えますが、単純なスタイリングであればContainer Scroll-State Queriesのsnapped
を用いたほうがシンプルな実装となるでしょう。
stuck
stuck
は、position: sticky
である要素が、コンテナで固定状態であるかをもとに判定を行います。
position: sticky
の代表的なユースケースとして、ヘッダーの固定が挙げられます。Container Scroll-State Queriesのstuck
と組み合わせると、ヘッダーが固定状態にある場合のみスタイルを切り替える、といったことが可能となります。
<body>
<header>
<div class="headerTitle">Header</div>
</header>
<div class="content">1</div>
<div class="content">2</div>
<div class="content">3</div>
<div class="content">4</div>
<div class="content">5</div>
</body>
/* 見た目の調整用のスタイルは省略 */
body {
padding-top: 32px;
}
header {
/* スクロール時にヘッダーを上部で固定 */
position: sticky;
top: 0px;
padding: 16px;
background-color: white;
/* コンテナとして定義 */
container-type: scroll-state;
/* 非固定状態時のスタイル */
.headerTitle {
padding: 8px;
font-size: 2em;
transition: all 0.2s;
}
/* sticky による固定状態時のスタイル */
@container scroll-state(stuck: top) {
.headerTitle {
border: 2px solid gray;
border-radius: 8px;
font-size: 1.2em;
}
}
}
snapped
と同様、position: sticky
である要素をコンテナとして定義する必要があり、かつ、その子要素に対して固定時のスタイルを適用しています。
動作例は次のとおりです。

スクロールしposition: sticky
であるヘッダーが固定状態になると、ヘッダーのスタイルが切り替わっていることがわかります。
ブラウザでのサポート状況
今回紹介した機能について、本記事掲載時点でのメジャーブラウザにおける利用可能バージョンは次のとおりです。これ以降のバージョンであれば利用可能です。
機能 | Chrome | Edge | Safari | Firefox |
---|---|---|---|---|
scrollbar-width |
121 | 121 | 18. |
64 |
scrollbar-color |
121 | 121 | (未対応) | 64 |
scrollbar-gutter |
94 | 94 | 18. |
97 |
::scroll-button() 擬似要素 |
135 | 135 | (未対応) | (未対応) |
::scroll-marker 擬似要素 |
135 | 135 | (未対応) | (未対応) |
::scroll-marker-group 擬似要素 |
135 | 135 | (未対応) | (未対応) |
Scroll Snap Events | 129 | 129 | (未対応) | (未対応) |
Container Scroll-State Queries - scrollable |
133 | 133 | (未対応) | (未対応) |
Container Scroll-State Queries - snapped |
133 | 133 | (未対応) | (未対応) |
Container Scroll-State Queries - stuck |
133 | 133 | (未対応) | (未対応) |
まとめ
今回は、スクロール関連の新しい機能を紹介しました。
従来JavaScriptで複雑な実装が必要だったケースの多くが、CSSのみでシンプルに実装できるようになりつつあります。現状では一部のブラウザしか使えない機能も多いですが、今後の仕様策定およびブラウザサポートが進めば、徐々に利用可能なケースも広がっていくでしょう。
すぐに活用はできずとも、知識として持っていると選択肢は広がるかと思いますので、ざっくりとでも押さえておくと良いでしょう。