メインコンテンツまでスキップ

スクロールバーにまつわるエトセトラ

CSS

広告

何かで役立つかもしれないスクロールバーのTips。

スクロールバーのCSSの仕様

スクロールバーの見た目を変更するCSSには現在2つの方法があります。

  • W3Cが定めた標準のプロパティを使用する
  • -webkit-ベンダープレフィックスがついたプロパティを使用する

2024年3月現在、W3C仕様のプロパティを使用する方法はSafariが、-webkit-ベンダープレフィックスを使用する方法はFirefoxが非対応となっています。

ただし、異なるブラウザ間での動作を統一するための「Interop 2024」というプロジェクトの項目にScrollbar Stylingが追加されたことで、将来的にはSafariもW3Cの標準プロパティをサポートし始め、スクロールバーの見た目を変更する方法はW3Cの標準に統一される可能性が高いです。また、-webkit-ベンダープレフィックスを使用する方法は将来的には廃止される予定となっております。

スクロールバーを非表示にする

スクロールは有効にしつつスクロールバーを非表示にする方法は次のとおりです。

@supports not selector(::-webkit-scrollbar) {
.scroller {
scrollbar-width: none;
}
}
@supports selector(::-webkit-scrollbar) {
.scroller::-webkit-scrollbar {
display: none;
}
}

自前でカルーセルスライダーを作成する時などに役立つかもしれません。

スクロールバーのデザインを変える

スクロールバーのデザインを変える場合は次のような指定でカスタマイズできます。

@media not (forced-colors: active) {
.scroller {
--size: 8px; /* スクロールバーの幅 */
--thumb-color: var(--color-primary); /* スクロールバーの移動する部分の色 */
--thumb-color-active: var(--color-primary-active); /* スクロールバーの移動する部分がホバー状態の時の色 */
--track-color: color-mix(in srgb, var(--color-primary) 20%, white); /* スクロールバーの背景色 */
--thumb-radius: 8px; /* スクロールバーの移動する部分の角丸 */
--track-radius: 8px; /* スクロールバーの背景部分の角丸 */
}
@supports not selector(::-webkit-scrollbar) {
.scroller {
scrollbar-color: var(--thumb-color) var(--track-color);
scrollbar-width: thin; /* 初期値のautoと選択 */
}
}
@supports selector(::-webkit-scrollbar) {
.scroller::-webkit-scrollbar {
inline-size: var(--size); /* 横書き時 縦スクロール (=width) */
block-size: var(--size); /* 横書き時 横スクロール (=height) */
}
.scroller::-webkit-scrollbar-thumb {
background-color: var(--thumb-color);
border-radius: var(--thumb-radius);
}
.scroller::-webkit-scrollbar-thumb:hover {
background-color: var(--thumb-color-active);
}
.scroller::-webkit-scrollbar-track {
background-color: var(--track-color);
border-radius: var(--track-radius);
}
}
}

スクロールバーのスタイリングは強制カラーモードが有効ではない時に適用する

強制カラーモードが有効な場合、スクロールバーのスタイリングを行うとスクロールバーが表示されなくなる危険性を孕んでいます。

forced-colorsメディア特性を使用して、強制カラーモードが有効ではない場合にのみスクロールバーのスタイリングを適応するようにしてください。

❗MUST
@media not (forced-colors: active) {
/* スクロールバーのCSS */
}
参考:Don’t use custom CSS scrollbars – Eric Bailey

参考記事内ではスタイリングされたスクロールバーは一貫性を欠き、システム設定やユーザーの好みに従わないためアクセシビリティに困難をもたらす可能性があると指摘しています。なるべくであればスクロールバーのスタイリング自体を避けたほうが良いかもしれません。

W3C仕様

  • scrollbar-colorは1つ目の値にスクロールバーの移動する部分の色を指定、2つ目の値にスクロールバーの背景を指定します。
  • scrollbar-widthに関しては標準のautoと細くするthinの2択です。-webkit-ベンダープレフィックスを使用する方法と違ってスクロールバーの幅をピクセル単位で変更することはできません。

上記CSSを指定したW3C仕様を使用したスクロールバーの表示は次のようになります。

上記指定をしたFirefoxでのスクロールバーのスクリーンショット

-webkit-ベンダープレフィックスを使用する方法

  • ::-webkit-scrollbar疑似要素でスクロールバーの幅を指定します。widthで縦スクロール時の幅、heightで横スクロール時の幅を指定します。
  • ::-webkit-scrollbar-thumb疑似要素でスクロールバーの移動する部分のスタイルを指定します。background-colorで色を、border-radiusで角丸を調整できます。また:hover擬似クラスでホバー時の色も設定できます。
  • ::-webkit-scrollbar-track疑似要素でスクロールバーの背景部分のスタイルを指定します。こちらもbackground-colorで色を、border-radiusで角丸を調整できます。

上記CSSを指定した-webkit-ベンダープレフィックスを使用したスクロールバーの表示は次のようになります。

上記指定をしたSafariでのスクロールバーのスクリーンショット

他にも::-webkit-scrollbar-track-piece::-webkit-scrollbar-cornerといった疑似要素もありますが、上記3点で事足りるので使用する機会は少ないです。

スクロールバーの幅を取得する

スクロールバーの幅はJSで取得し、CSSカスタムプロパティに格納しておくのがおすすめです。

// スクロールバーの幅をCSSに格納する関数
const updateScrollBarWidth = () => {
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth
document.documentElement.style.setProperty('--scrollbar-width', `${scrollBarWidth}px`)
}
// debounce関数
const debounce = <T extends any[], R>(callback: (...args: T) => R): ((...args: T) => void) => {
let timeout: number | undefined
return (...args: T): void => {
if (timeout !== undefined) cancelAnimationFrame(timeout)
timeout = requestAnimationFrame(() => callback.apply(this, args))
}
}
window.addEventListener('resize', debounce(updateScrollBarWidth))
window.addEventListener('load', updateScrollBarWidth)
  • ブラウザウインドウの内部幅(window.innerWidth)とドキュメントのクライアント領域の幅(document.documentElement.clientWidth)の差を計算しています。この差分は、実質的にブラウザのスクロールバーの幅に相当します。計算されたスクロールバーの幅は、CSSカスタムプロパティ--scrollbar-widthとして:root要素に設定されます。
  • ウインドウのリサイズ中に不必要に多くの計算が行われるのをdebounce()関数で防ぎます。
  • ページのロード時とウインドウサイズの変更時にupdateScrollBarWidth()を実行します。

Xのポストではhtml要素にcontainerを指定することでCSSのみでスクロールバーの幅を取得する方法が紹介されており、そこそこ反響を得ていましたが、html要素にcontainerを指定すると子孫要素のposition:fixedが軒並み死滅するので絶対にこの方法を使用してはいけません。そのため、現状ではJSを使用する以外に選択肢はないと考えています。

このCSSカスタムプロパティが役立つのは次のようなパターンの場合です。

子要素を親要素からはみ出して画面いっぱいにする場合

子要素を親要素からはみ出して画面いっぱいに表示する方法として有名なのは以下の方法でしょう。

.contents {
margin-inline: calc(50% - 50vi);
}

参考:子要素を親要素(インナー幅)からはみ出して画面いっぱいにするCSS | HPcode(えいちぴーこーど)

この方法を使用する場合、vi(vw)単位はスクロールバーの領域を含めた横幅であるため横スクロールバーを生み出してしまうのが懸念点となります。

横スクロールバーの表示を防止しながら子要素を親要素からはみ出して画面いっぱいに表示する場合、50viからスクロールバーの幅を2で割ったものを引くことで実現できます。

.contents {
margin-inline: calc(50% - (50vi - var(--scrollbar-width, 0) / 2));
}

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

モーダルが開いている際に背面のスクロールを防止する場合

モーダルを開いている際の背面のスクロールを防止するにはJSの利用が必須でしたが、:has()疑似クラスが普及した現在ではiOSでは完全にスクロールを無効にできないことを許容すればCSSのみで背面のスクロールを防止できます。

/* dialog要素が開いている場合 */
body:has(dialog[open]) {
overflow: hidden;
}

この記述だけだと背面固定時にスクロールバーが消失する都合上、背面のコンテンツにガタツキが生じてしまいます。スクロールバーの幅を事前に取得しておき、スクロールバーが出現する場所に幅分の余白を設けることで背面コンテンツのガタツキを防ぐことができます。

/* dialog要素が開いている場合 */
body:has(dialog[open]) {
overflow: hidden;
padding-inline-end: var(--scrollbar-width, 0);
}

スクロールバーが表示されていない状態でもスクロールバー分の余白を確保できるscrollbar-gutterというプロパティがありますが、こちらはW3C仕様のプロパティのため現在はSafariで利用することができません。将来的にSafariでもW3C仕様のプロパティがサポートされたらscrollbar-gutterを使用するのがベターかと思います。

body {
overflow-y: scroll;
}
body:has(dialog[open]) {
overflow: hidden;
}
/* header, main, footerを囲むラッパー要素 */
.page-wrapper:has(dialog[open]) {
overflow-y: auto;
scrollbar-gutter: stable;
}

iOSでも完全にスクロールを無効化したい場合は過去にZennに投稿した記事「モーダルを開いている時に背面コンテンツのスクロールを抑制する方法」で紹介している手法を利用してください。

システムの外観モードに合わせてスクロールバーの配色を変更する

次のmeta要素をhead内に配置することで、スクロールバーの配色がシステムの外観モードに合った色に変更されます。

<meta name="color-scheme" content="light dark" />

ただし、当ブログのようにシステム設定関係なくスイッチでライトモード・ダークモードを切替可能としている場合はそのままではスクロールバーの色がテーマに合致しなくなります。合致させる場合はmeta[name="color-scheme"]の値をdarkもしくはlightに同時に切り替える必要があります。

当ブログのテーマ切り替えスイッチで使用しているJSの一部抜粋ですが、以下のように指定しておくと良さそうです。

const button = document.getElementById('js-site-theme-switch') as HTMLButtonElement
const meta = document.querySelector('meta[name="color-scheme"]') as HTMLMetaElement
const toggleTheme = (): void => {
try {
// 現在のテーマをセッションストレージから取得
const currentTheme = localStorage.getItem('theme') || 'light'
// テーマを切り替え
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
// 新しいテーマをローカルストレージに保存
localStorage.setItem('theme', newTheme)
document.documentElement.setAttribute('data-theme', newTheme)
if (meta) meta.setAttribute('content', newTheme)
} catch (error) {
console.error(error)
}
}
if (button) {
button.addEventListener('click', toggleTheme)
}

横スクロールバーが出るのを防止する

数日前に投稿した記事「横スクロールバーの発生源を素早く特定する方法と最新の防止策」をお読みください。