モーダル表示時のスクロールバー調整やオーバレイ表示を極力JavaScriptなしで実装する方法

4 min read

前提

モーダルは<dialog>要素を使用するが、<div>で実装したものでも問題ない。

構図は以下を想定する。

<body>
  <dialog></dialog>
  <div class="overlay"></div>
</body>
dialogのスタイル例
dialog {
  position: fixed;
  isolation: isolate;
  content-visibility: hidden;
}

dialog[open] {
  z-index: 1;
  padding: 0;
  border: none;
  content-visibility: visible;
}

モーダル表示時のオーバーレイを兄弟セレクタで表示する

dialogが開いたときにopen属性が付与される。それを利用してオーバーレイを表示する。

.overlay {
  position: fixed;
  inset: 0;
  z-index: -1;
  visibility: hidden;
  background-color: var(--overlay-backgrounds);
  opacity: 0;
}
dialog[open] ~ .overlay {
  visibility: visible;
  opacity: 1;
}

隣接セレクタではなく兄弟セレクタを利用しているのは、dialog.overlayの間にサードパーティ製の要素が挟まることを考慮しているためである。

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

:hasセレクタを利用することでdialogが表示されている(open属性)際、body要素にoverflow: hiddenを適用できる。

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

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

macOSの設定によってはスクロールバーは常に表示されないため気付かれないことも多いが、スクロールバーを非表示にすると横幅分のスペースがズレてしまう。

スクロールバーの横幅自体はwindow.innerWidth - document.documentElement.clientWidthで取得できる。これをCSS Custom Propertiesに格納しておき、スタイルで利用できるようにしておく。

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

window.addEventListener('load', setScrollbarWidth);
body:has(dialog[open]) {
  overflow: hidden;
  padding-right: var(--scrollbar-width, 0);
}

var()関数の第二引数は、カスタムプロパティが未定義の場合に適用される値である。不正な値の場合に予想外の数値が適応されないように0を指定しておく。

論理プロパティとしてpadding-inline-endを利用をしても良い。

position: fixed要素にスクロールバー分の余白を付与する

bodyに対して余白を付与する形を取ったが、サイトのUIによっては問題がある。例えば、固定ヘッダやトップに戻るボタンのようなposition: fixedなどで固定されたUIの場合、そのUIだけスクロールバー分の余白がカクついてしまう。
命名は何でも良いが、data-floating属性というものを用意して、対象の要素の付与してスクロールバー分の余白を付与する。

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

body:has(dialog[open]),
body:has(dialog[open]) [data-floating] {
  padding-right: var(--scrollbar-width, 0);
}
別の例

見通しが悪そうなため、ネスト記法が使えるケース(SCSSなど)を記載する。

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

    &,
    [data-floating] {
      padding-right: var(--scrollbar-width, 0);
    }
  }
}
<body>
  <header data-floating></header>
  <dialog></dialog>
  <div class="overlay"></div>
</body>

おわり

スクロールバーの横幅を取得する部分だけJavaScriptを利用しているが、それ以外はCSSだけで実装できる。

:has()セレクタが使えない場合、モーダルの表示状態をJavaScriptで監視して、body要素にクラスを付与するなどの処理が必要になるため、それがなくなるだけでも実装が簡単になる。