CSSで、スクロールバーの有無でレイアウトがずれないようにする方法

​​CSS​HTML,Playwright

ページが長くなってスクロールバーが出ると、その幅の分だけページの横幅が狭くなり、中央に寄せたヘッダーや本文が左へずれます。html に scrollbar-gutter: stable を指定すると、スクロールバーが出ていないときも同じ幅を空けておくので、ページを切り替えても中身の位置は変わりません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。幅800px・高さ600pxの画面でページを開き、要素の位置と幅を getBoundingClientRect() で測りました。Playwright はヘッドレスで起動するとき、Chromium に --hide-scrollbars を付けます。そのままでは、長いページでも body の幅は800pxのままでした。そこでこの引数を外し、幅を取る通常のスクロールバー(この環境では15px)を出して測っています。Firefox と Safari、Windows と macOS、スマートフォン、中身の上に重ねて表示するスクロールバーの環境では試していません。

上下に同じページを2つ並べ、上のボタンを4回押して本文を増やしたり減らしたりする。上の「指定なし」では、本文が増えて右にスクロールバーが出るたびに、ロゴと本文が点線より左へずれ、ロゴの左端の数字が80pxと72.5pxのあいだで変わってピンクになる。下の「html { scrollbar-gutter: stable; }」では、本文が短いときも右に空白があり、ロゴは点線の位置の72.5pxのまま動かない
本文を増やす・減らすボタンを4回押したときの、ロゴの位置(カーソル、押した瞬間の円、回数、数字、点線は GIF 用に重ねたもの。iframe の幅は720px)

ずれるのは、スクロールバーの幅だけページが狭くなるから

検証に使ったのは、ヘッダーと本文を、幅560pxまでの入れ物で中央に寄せたページです。

css
/* 中央に寄せた、幅560pxまでの入れ物 */
.container {
  max-width: 560px;
  margin: 0 auto;
  padding: 0 16px;
}
html
<header class="site-header">
  <div class="container">
    <span class="logo">それなら猫の手で</span>
    <nav class="menu">……</nav>
  </div>
</header>
<main class="container">
  <p>本文の段落 1。……</p>
</main>

本文の段落を1つだけにした短いページと、40個にした長いページで、body の幅と、ヘッダーの入れ物の左端を測りました。

ページ スクロールバー body の幅 ヘッダーの入れ物の左端
短い なし 800px 104px
長い あり 785px 96.5px

長いページでは、右にスクロールバーが出て、body の幅が15px狭くなりました。入れ物は残った幅の真ん中に置かれるので、左右の余白が7.5pxずつ減り、左端が104pxから96.5pxへずれます。記事一覧のような短いページと、記事のような長いページを行き来すると、ロゴやメニューがそのたびに横に動いて見えます。

html に scrollbar-gutter: stable を指定する

ずれを止めるには、html に次の1行を足します。

css
html {
  scrollbar-gutter: stable;
}
html の指定 短いページの body の幅 短いページの左端 長いページの body の幅 長いページの左端
指定なし 800px 104px 785px 96.5px
scrollbar-gutter: stable 785px 96.5px 785px 96.5px

短いページでも body の幅が785pxになり、長いページと同じ位置に中身が置かれました。スクロールバーが出ていないときは、右端の15pxが何もない余白になります。

MDN の scrollbar-gutter の説明によると、このプロパティはスクロールバーを置く場所(ガター)を前もって空けておくためのものです。stable を指定すると、中身があふれていなくても、overflow が auto、scroll、hidden のどれかであればガターができます。

指定する場所は html です。同じ1行を body に書いたところ、短いページの左端は104pxのままで、ずれは止まりませんでした。CSS Overflow Module Level 3 の仕様では、ルート要素(html)に指定した scrollbar-gutter は画面全体のスクロールに使われ、overflow と違って body の指定は画面全体に引き継がれない、と決められています。

overflow-y: scroll でもずれは止まるが、空の枠が出る

ほかの方法として、html に overflow-y: scroll を指定して、スクロールバーを常に出しておくやり方もあります。位置を測ると、scrollbar-gutter: stable と同じ結果になりました。

html の指定 短いページの左端 長いページの左端
overflow-y: scroll 96.5px 96.5px
scrollbar-gutter: stable 96.5px 96.5px

違いは見た目です。overflow-y: scroll では、スクロールする必要のない短いページにも、スクロールバーの枠と上下の矢印が出ます。scrollbar-gutter: stable では、同じ場所は白い余白になります。

上下2段の比較。どちらも本文が1段落だけの短いページ。上の「html { overflow-y: scroll; }」では、右端に灰色のスクロールバーの枠と上下の矢印が出ている。下の「html { scrollbar-gutter: stable; }」では、右端は何もない余白で、ヘッダーの水色の背景がその手前で終わっている
短いページで、overflow-y: scroll(上)と scrollbar-gutter: stable(下)を比べた画面。違いは右端の15pxの部分

中身を画面の真ん中に保つなら both-edges を付ける

stable だけでは、短いページでも中身が7.5px左に寄った位置に置かれます。画面の真ん中からずらしたくないときは、both-edges を付けて、反対側(左)にも同じ幅を空けます。

css
html {
  scrollbar-gutter: stable both-edges;
}
html の指定 body の幅 短いページの左端 長いページの左端
stable 785px 96.5px 96.5px
stable both-edges 770px 104px 104px

both-edges では、左右に15pxずつ空いて body の幅は770pxになり、左端はどちらのページでも104pxでした。スクロールバーのない短いページを、指定なしで開いたときと同じ位置です。そのかわり、body の幅は stable より15px狭くなるので、body の中の要素は、画面の左右15pxずつの部分には広がりません。

モーダルを開いて overflow: hidden にしてもずれない

モーダルを開いている間は、後ろのページがスクロールしないように、html に overflow: hidden を付けることがあります。このときもスクロールバーが消えるので、後ろのページが右へずれます。長いページで、overflow: hidden を付ける前と後の左端を測りました。

html の指定 付ける前 overflow: hidden を付けた後
指定なし 96.5px 104px
scrollbar-gutter: stable 96.5px 96.5px

scrollbar-gutter: stable を指定しておけば、スクロールバーが消えたあともガターが残り、後ろのページは動きませんでした。先に引いた MDN の説明のとおり、overflow: hidden でも stable のガターはできます。スクロールバーの幅を JavaScript で測って padding-right を足す、といった処理は要りません。

幅 100vw の要素も、はみ出さなくなる

画面の端から端まで色を塗る帯などに width: 100vw を使うと、スクロールバーのある長いページでは、帯がスクロールバーの分だけはみ出して、横にもスクロールできるようになります。html の指定ごとに、帯の幅と横へのはみ出しを調べました。

html の指定 短いページの帯の幅 長いページの帯の幅 長いページの body の幅 長いページで横にはみ出すか
指定なし 800px 800px 785px はみ出す
scrollbar-gutter: stable 785px 785px 785px はみ出さない
overflow-y: scroll 785px 785px 785px はみ出さない

scrollbar-gutter: stable や overflow-y: scroll を指定すると、100vw はガターを除いた785pxになり、body の幅と同じになりました。CSS Values and Units Module Level 4 の仕様では、ルート要素の overflow や scrollbar-gutter によって、スクロールバーやその場所が常に出る場合は、vw などの単位をその分だけ小さくする、と決められています。仕様の変更履歴によると、scrollbar-gutter についてのこの決まりは、2023年12月18日の草案のあとに加わったものです。検証環境の Chromium 153 では仕様のとおりに動きましたが、100vw を使う場合は、対象のブラウザで帯の幅を確かめてください。

ページの中のスクロールする箱にも使える

scrollbar-gutter は、ページ全体だけでなく、高さを決めてスクロールさせるリストなどにも使えます。幅300px・高さ200pxのリストに、高さ30pxの項目を3個入れた場合と12個入れた場合で、項目の幅を測りました。

css
.list {
  width: 300px;
  height: 200px;
  overflow-y: auto;
  scrollbar-gutter: stable;
}
リストの指定 項目3個 項目12個
overflow-y: auto だけ 298px 283px
overflow-y: auto と scrollbar-gutter: stable 283px 283px
scrollbar-gutter: stable だけ 298px 298px

項目が増えてスクロールバーが出ても、項目の幅は283pxのままでした。ただし、overflow を書かずに scrollbar-gutter: stable だけを指定したリストでは、幅は空きませんでした。MDN の説明にあるとおり、ガターができるのは overflow が auto、scroll、hidden のときです。html の場合は、先に引いた CSS Overflow Module Level 3 の仕様で、画面全体の overflow: visible は auto として扱うと決められているので、scrollbar-gutter だけで足ります。

対応しているブラウザと、ガターができない環境

MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で調べた、最初に対応したバージョンです。

機能 Chrome Firefox Safari iOS の Safari
scrollbar-gutter(stable を含む) 94 97 18.2 18.2

MDN の説明によると、スクロールバーには、場所を取る従来のものと、中身の上に重ねて表示するオーバーレイのものがあり、どちらを使うかはブラウザが決めます。オーバーレイのスクロールバーは場所を取らないので、stable を指定してもガターはできません。初期値の auto でも場所を取らないので、そもそもスクロールバーの有無で幅は変わりません。

​Posted by 86