モーダルを開いている時にページがスクロールしてしまうのを防ぐCSSとJavaScriptのテクニック
Post on:2019年6月18日
ページ上でモーダルを開き、スクロールして、モーダルを閉じると、通常そのページはモーダルを開いた時とは別の場所にスクロールされた状態で表示されてしまいます。そして、スクロールした状態で、モーダルを開いて閉じると、一番上にスクロールされた状態で表示されてしまいます。
これらを解決するCSSとJavaScriptのテクニックを紹介します。
Prevent Page Scrolling When a Modal is Open
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
はじめに
モーダルを開いて、スクロールして、モーダルを閉じます。すると、そのページはモーダルを開いた時とは別の場所にスクロールされた状態で表示されています。
下記のデモで試してみてください。
See the Pen
Avoid body scrollable in safari when modal dialog shown by Geoff Graham (@geoffgraham)
on CodePen.
この原因は、モーダルも他と同じようにページ上の一つの要素だからです。ページは元の位置に保持されますが(それが本来の目的だと仮定すると)、残りのページは通常通りにスクロールが動作します。
ビューポートの高さがスクリーンいっぱいであれば、問題にはなりません。しかし、そうではない場合があります。CSS(そしてJavaScript)を使用してこの問題を解決したいと思います。
簡単なことから始めましょう
body全体の高さをビューポートの高さいっぱいに定義し、モーダルが開いている時に垂直方向のオーバーフローをhiddenにすることで、このopen-modal-page-scrolling™問題に大きな影響を与えることができます。
1 2 3 4 |
body.modal-open { height: 100vh; overflow-y: hidden; } |
これで問題ありませんが、モーダルを開く前にbody要素をスクロールした場合は、横方向に少しだけリフローします。ビューポートの幅は、スクロールバーの幅と同じ15pxほど広くなります。
See the Pen
Avoid body scrollable in safari when modal dialog shown by Geoff Graham (@geoffgraham)
on CodePen.
これを避けるために、body要素に右側の余白を加えて調整します。
1 2 3 4 5 |
body { height: 100vh; overflow-y: hidden; padding-right: 15px; /* 幅のリフローを避ける */ } |
このテクニックを使う場合は、モーダルがビューポートの高さよりも短くなければならないことに注意してください。長い場合は、bodyのスクロールバーが必要になります。
スマホの場合
上記のテクニックは、デスクトップとAndroidのスマホで非常にうまく機能します。iOS用Safariでは、タッチスクリーンをタップして動かしているときにモーダルが開いているとbodyがスクロールするので、もう少し手を加えます。
回避策として、bodyをfixedにします。
1 2 3 |
body { position: fixed; } |
これだけで上手く機能します! スクリーンにタッチしてもbodyは反応しません。しかし、まだ小さな問題があります。
モーダルのトリガーがページの下にあるとします。クリックして、モーダルを開くと言うでしょう。しかし、モーダルを閉じると、自動的に画面の一番上までスクロールしています。この問題は、解決しようとしているスクロールの動作と同じぐらいユーザーを混乱させます。
See the Pen
Avoid body scrollable in safari when modal dialog shown by Geoff Graham (@geoffgraham)
on CodePen.
最初に一番下までスクロールして、それからモーダルを開いてみてください。モーダルを閉じると、一番上にスクロールされた状態になってしまいます。
この問題を解決するにはJavaScriptが必要
JavaScriptを使用すると、タッチイベントのバブルを回避できます。モーダルが開いている時には、背景レイヤーがあることは誰もが知っています。残念ながら、iOSではstopPropagationは扱いにくいですが、preventDefaultはうまく動作します。つまり、背景やモーダルボックスのレイヤーだけでなく、モーダルに含まれるすべてのDOMノードにイベントリスナーを追加する必要があります。幸いなことに、jQueryを含め、多くのJavaScriptライブラリで簡単に対応できます。
もう1つ、モーダル内をスクロールする必要がある場合はどうすればよいでしょうか。タッチイベントの応答をトリガーにしなければなりませんが、モーダルの最上部または最下部に到達した場合でも、バブリングを防ぐ必要があります。非常に複雑な作業のように見えますが、まったく問題がないわけではありません。
JavaScriptで問題を解決する
まずは、bodyに定義したCSSを見てましょう。
1 2 3 |
body { position: fixed; } |
スクロールした位置を取得して、その位置をCSSに追加すれば、bodyはスクロールしてスクリーンの一番上まで戻ってこないので、問題は解決します。JavaScriptを使用してスクロールのトップを取得し、その値をbodyのスタイルに追加します。
1 2 3 4 5 6 7 |
// モーダルが開いたら、bodyにfixedを付与 document.body.style.position = 'fixed'; document.body.style.top = `-${window.scrollY}px`; // モーダルが閉じられたら、スクロールのトップを取得してbodyに document.body.style.position = ''; document.body.style.top = ''; |
これは機能しますが、モーダルが閉じられた後にも少しリークがあります。具体的には、モーダルが開いていてbodyにfixedが設定されている場合、ページはスクロール位置を失います。そのため、その位置を取得しなければなりません。JavaScriptを少し修正します。
1 2 3 4 5 |
// モーダルが閉じられ時... const top = document.body.style.top; document.body.style.position = ''; document.body.style.top = ''; window.scrollTo(0, parseInt(scrollY || '0') * -1); |
これで完成です!
モーダルが開いている時はbodyがスクロールしなくなり、モーダルが開いている時も閉じている時もスクロール位置が維持されるようになりました。
See the Pen
Avoid body scrollable in safari when modal dialog shown by Geoff Graham (@geoffgraham)
on CodePen.
sponsors