Javascriptでwindowsのスクロールバー問題を解決する方法

windowsとMacではスクロールバーの初期設定の違いがあるのはご存知でしょうか?

スクロールバーの設定の違いでMacでサイト制作をしていると気づきずらいのですが、ハンバーガーメニューなどを開いた時にスクロールを強制的に停止した場合、windowsでレイアウトがずれてしまいます。下記のcodepenで確認してみてください。

See the Pen blog_scroll_bar by 殿村 真史 (@ryngytsw-the-encoder) on CodePen.

上記の例ではheaderタグにはwidth: 100%;を設置して、mainタグにはwidth: 100vw;を設定していて、width: 100vwはブラウザ幅の領域なので、スクロールバーの幅がプラスされてしまって横スクロールが発生してしまっています。

スクロール停止ボタンを押すとheaderのレイアウトがズレると思います。

この原因はデフォルトの設定でwindowsのスクロールバーはビューポート内に横幅が確保されていて、Macでスクロールバーの横幅の領域確保されてないので、スクロール停止時(bodyタグにoverflow: hiddenを動的に付加するなど)にスクロールバーが消えることでwindowsのみレイアウトがずれます。

まとめると下記の状況となっております。

width: 100% → スクロールバーの幅を含んでないので、スクロールバーが無くなると広がる形でズレる

width: 100vw → スクロールバーの幅を含んでいるので、body幅を超えてしまうので横スクロールが発生する

Macを使っている場合、この挙動にいち早く気づくためにはMacの設定でwindows環境と同じスクロールバーの設定にしておくといいです。

「環境設定」→「外観」→ 「スクロールバーを表示」の項目で「常に表示」に変更しておくとwindowsと同じ仕様のスクロールバーとなり、レイアウトのズレをすぐに気づくことができます。

CSSのscrollbar-gutterプロパティで解決を試みる

See the Pen Untitled by 殿村 真史 (@ryngytsw-the-encoder) on CodePen.

:root {
  --scrollbar-width: 0;
}

* {
  box-sizing: border-box;
}

html, body {
  padding: 0;
  margin: 0;
}

body.is-notScroll {
  overflow: hidden;
}

body.is-notScroll .header {
  overflow: auto; /* スクロールバー対策 */
  scrollbar-gutter: stable; /* スクロールバー対策 */
}

body.is-notScroll main {
}

.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: gray;
  padding-inline: 20px;
}

.header ul {
  display: flex;
  align-items: center;
  gap: 10px;
}

li {
  list-style: none;
}

main {
  width: calc(100vw - 15px); /* スクロールバー対策 */
  padding-top: 56px;
  padding-inline: 20px;
}

.section {
  max-width: 100%;
  width: 100%;
  height: 150vh;
}

.section__inner {
  background: green;
  width: 100%;
  height: 100%;
  padding-top: 100px;
}

p {
  color: #fff;
}

19、18行目でスクロールバー対策用にoverflow: auto; scrollbar-gutter: stable;というプロパティを追加しました。このプロパティを追加することで、スクロールバーが消えてもスクロールバーの幅を保持してくれます。これでheaderのwidth: 100%;については解決できそうです。

49行目のmainタグではスクロールバーの幅を15pxと仮定して、100vwから15pxを引いておくことで横スライドを解決することができました。スクロールバーを非表示にしてもズレは無いです。

mainタグについて解決してそうですが、Macでスクロールバー設定を「常に表示」の設定を「常に表示しない設定」に戻すと右側にスクロールバー分の余白広がって表示されてしまいます。

うーん、もどかしい。。

原因はwidth: 100vwから15pxの固定値を引いているので、windowsではスクロールバーの幅が確保されているので変ではないが、Macでは確保されてないので、右の余白が開いてしまったようです。

まずスクロールバーは15pxとは限らないのと、スクロールバーの横幅を動的に取得してMacの場合は0pxになるといい感じになりそうです。

JavaScriptでスクロールバーの幅を動的に取得して対応してみよう

See the Pen blog_scroll_bar_js by 殿村 真史 (@ryngytsw-the-encoder) on CodePen.

Javascriptで動的にスクロールバーの幅を取得して、対応してみました。上記のcodepenでいい感じになりました。

CSSの変更部分をみてみる

:root {
  --scrollbar-width: 0;
}

* {
  box-sizing: border-box;
}

html, body {
  padding: 0;
  margin: 0;
}

body.is-notScroll {
  overflow: hidden;
}

body.is-notScroll .header {
  padding-inline-end: calc(20px + var(--scrollbar-width, 0));
}

body.is-notScroll main {
}

.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: gray;
  padding-inline: 20px;
}

.header ul {
  display: flex;
  align-items: center;
  gap: 10px;
}

li {
  list-style: none;
}

main {
  width: calc(100vw - var(--scrollbar-width, 0));
  padding-top: 56px;
  padding-inline: 20px;
}

.section {
  max-width: 100%;
  width: 100%;
  height: 150vh;
}

.section__inner {
  background: green;
  width: 100%;
  height: 100%;
  padding-top: 100px;
}

p {
  color: #fff;
}

2行目で--scrollbar-width: 0;としてカスタムプロパティに動的な値を入れる用意をしておきます。初期値を設定しておくことで、スクロールバーの無い環境では0が適用されます。

19行目のheaderタグではoverflow: auto; scrollbar-gutter: stable;を削除して、padding-inline-end: calc(20px + var(--scrollbar-width, 0));という指定に変更して、動的にスクロールバーが無くなった時にpaddingの右側に動的に取得したスクロールバーの値を入れる設定にしておきました。

48行目のmainタグではwidth: calc(100vw - var(--scrollbar-width, 0));として、100vwから動的なスクロールバーの幅を引き算する設定にしています。

これでMacではスクロールバーの幅は0pxを取得して、windows環境では動的なスクロールバーの値を取得してそれを引いたり足したりできるようになりました。

Javascriptで動的にスクロールバーの値を取得する

//スクロール停止ボタンの挙動の関数
const offScroll = () => {
  document.body.classList.toggle('is-notScroll');
}

// スクロールバーの幅をCSSに格納する関数
const updateScrollBarWidth = () => {
  const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth
  document.documentElement.style.setProperty('--scrollbar-width', `${scrollBarWidth}px`)
}

// debounce関数
const debounce = (callback) => {
  let timeout;

  return function(...args) {
    const context = this;
    
    if (timeout !== undefined) cancelAnimationFrame(timeout)
    timeout = requestAnimationFrame(() => callback.apply(context, args));
  }
}

window.addEventListener('resize', debounce(updateScrollBarWidth));
window.addEventListener('load', updateScrollBarWidth);

2行目のoffScrollはスクロール停止にする関数になります。

7行目のupdateScrollBarWidth関数はスクロールバーの幅を取得する関数となります。8行目でwindow.innerWidth(ビューポートの幅)からdocument.documentElement.clientWidth(<html>要素領域の幅)を引くことで、スクロールバーの幅を取得しています。9行目では取得したスクロールバーの幅をsetProperty()というメソッドを使うことで、CSSのカスタムプロパティ(CSS変数)にスクロールバーの幅をセットしてます。

13行目のdebounce関数は24行目のresizeイベントで発生する大量の発火イベントを最適化して、ブラウザに負担をかけないようにするための関数で、24行目でresizeイベント時にdebounce関数を挟み込むことで、処理の最適化をしています。

24行目は画面幅が動的に変わった時(リサイズ時)にスクロールバーの幅の値を取得する関数で、25行目が初期表示時にスクロールバーの幅の値を取得する関数となります。

下記はスクロールバーがある設定の動的なスクロールバーの値

下記はスクロールバーが無い設定の動的なスクロールバーの値

このようにすることで、JavaScriptで動的にスクロールバーの値を取得して、スクロールバー問題を解決することができました。
また疑問点やおかしな箇所などありましたら、記事のコメント欄やフォームなどからメッセージをいただけますとありがたいです。

この記事を書いた人

アバター

トノムラマサシ

masashi
Webサイト屋兼ブロガー|過去に企業常駐などを経て現在はフリーランスでディレクションとコーダーとして活動しております。JS大好きなのでJSの仕事依頼お待ちしております。京都在中で5人家族。