[モーダル] スクロールバー表示時のガタつきをなくす実装方法

3 min read

概要

モーダルウィンドウを表示または非表示にした際、スクロールバーが表示されることでコンテンツがズレるケースに対処する。

解決方法

1.スクロールバーの幅を取得

まずはスクロールバーのサイズを確保しておく。CSSから値を取得できるようにCSS Custom Propertiesにスクロールバーの幅を設定する。

function observeScrollbarWidth() {
  const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
  document.documentElement.style.setProperty('--scrollbar-width', `${scrollBarWidth}px`);
}

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

上記のようにロード時とリサイズ時にスクロールバーの幅を取得し、CSS Custom Propertiesに設定する。resizeの場合はdebounce関数を使ってリサイズ時のパフォーマンスを下げないようにしている。

2.スクロールバーの幅を考慮したスタイルを適用

今回はdialog要素が開いている時にスクロールバーの幅を考慮したスタイルを適用する例を示す。擬似クラスの:has()を使えば、CSSだけでモーダル表示時にスクロールバーを非表示にできる。

body {
  &:has(dialog[open]) {
    overflow: hidden;

    & {
      padding-inline-end: var(--scrollbar-width, 0);
    }
  }
}

dialog表示時にスクロールバーの幅分の余白をもたせる必要があるため、dialog表示時はbody要素にスクロールバーのサイズ分のpadding-inline-endを設定する。

プリプロセッサを利用していない(CSS)場合
body:has(dialog[open]) {
  overflow: hidden;
}

body:has(dialog[open]) {
  padding-inline-end: var(--scrollbar-width, 0);
}

CSSだけで実装をしていない場合は、JSで付与した状態(CSSクラス)に対して同じようにpadding-inline-endを適用すると良い。

const dialog = document.querySelector('dialog');

if (dialog && dialog.open) {
  document.body.classList.add('open-dialog');
} else {
  document.body.classList.remove('open-dialog');
}
body.open-dialog {
  padding-inline-end: var(--scrollbar-width, 0);
}

論理プロパティを利用している理由

padding-inline-endを使っている理由は、LTR(左から右)とRTL(右から左)の両方に対応するため、論理プロパティを指定した。もし柔軟に対応をしなくて良い場合などは、LTRの場合はpadding-right、RTLの場合はpadding-leftを使うことで同様に余白を設定できる。

その他

macOS開発者向けの設定

macOSはスクロールバーを設定で非表示にできるため、macOSを利用している開発者はスクロールバーのガタつきに気付きにくい。

  1. システム設定の「外観」
  2. 「スクロールバーを表示」を「常に表示」に変更

上記の設定をすることで、macOSでもスクロールバーの表示・非表示時のガタつきを確認できる。