🦴

【2023年】iOSにおけるモーダルウィンドウの背景固定(スクロール抑止)&下までスクロールできない問題の解決方法

2023/01/25に公開約5,000字

はじめに

今回は、モーダルウィンドウが開かれた時に背景を固定(背面コンテンツのスクロールを抑止)する方法を改めて紹介したいと思います。

以前まで、PCやAndroidでは比較的簡単に実現させることができましたが、iPhoneもしくはiPad(iOS or iPadOS)では別の方法で実装しなければ、実現できませんでした。

しかし、それはもう過去の話。今ではもう、簡単に実現できます🙆‍♂️

※ただし考慮すべき点はあるので、最後まで読み進めていただければ幸いです。

iOSにおけるモーダルの背景固定が楽になっている!?

モーダルウィンドウが開かれたときに、bodyoverflow: hidden;を設定することができれば、背景のスクロールを無効にできます。

body {
  overflow: hidden; /* モーダルが開かれた場合にのみ動的に設定する必要有り */
}

今まで、上記方法ではiOS端末だと背景固定できず、別の実装が必要でした(詳細は後述)

しかし、iOS16から、上記設定でもiOSで背景固定できるようになったみたいです。最高かよ。

また、overscroll-behaviorを使えば、開かれているモーダル内のスクロールのみを有効にできるようになりました。モーダルが開かれたときに、背景を固定するためのスタイルを動的に付与する必要もありません。神です。

<div class="modal">
  <div class="modal-contents"></div>
</div>
.modal {
  height: 300px;
  overflow-y: auto;
  overscroll-behavior-y: contain; /* スクロール可能な要素に付与する必要有り */
}
.modal-contents {
  height: 1000px;
}

【参考】モーダルUI等のスクロール連鎖を防ぐ待望のCSS

ただし、これもiOS16から利用できる方法です。

下記サイトを確認すると、iOS16未満のiPhoneを使っている人はまだまだ多そうなので、上記方法に頼り切るのは難しいところもありそうですね。
https://gs.statcounter.com/ios-version-market-share/mobile/worldwide/#monthly-202212-202301-bar

iOS16未満のiPhoneを考慮する場合

iOS16未満のiPhoneを考慮して実装する必要がある場合、以下2つの方法のいずれかで背景固定(背面コンテンツのスクロール抑止)を実現できます。

スクロール位置を保持した上でbodyposition: fixedを設定する

多くのWebサイトでは、下記記事で紹介されているような方法が使われています。私自身、以前勤めていたWeb制作会社でWebサイトを量産していたときは、共通処理化して汎用的に使えるようにしていました。
https://myscreate.com/optional-modal/

React(Next.js)で実現したい場合は、以下のようなカスタムフックuseBodyFixedを作成するのも良いかもしれません。

hooks/useBodyFixed.ts
import { useEffect, useRef, useState } from "react";

const isIOSUserAgent = () => {
  const ua = window.navigator.userAgent.toLowerCase();
  const isIOS =
    ua.indexOf("iphone") > -1 ||
    ua.indexOf("ipad") > -1 ||
    (ua.indexOf("macintosh") > -1 && "ontouchend" in document);
  return isIOS;
};

export const useBodyFixed = () => {
  const [bodyFixed, setBodyFixed] = useState<boolean>(false);
  const [scrollPosition, setScrollPosition] = useState<number>(0);
  const isFirstRender = useRef(false);

  useEffect(() => {
    // 初回レンダリング時は発火しないようにする
    if (!isFirstRender.current) {
      isFirstRender.current = true;
      return;
    }

    const body = document.querySelector("body");
    if (!body) return;

    const isIOS = isIOSUserAgent();

    if (bodyFixed) {
      if (isIOS) {
        setScrollPosition(window.pageYOffset);
        body.style.position = "fixed";
        body.style.top = `-${scrollPosition}px`;
      } else {
        body.style.overflow = "hidden";
      }
    } else {
      if (isIOS) {
        body.style.removeProperty("position");
        body.style.removeProperty("top");
        window.scrollTo(0, scrollPosition);
      } else {
        body.style.removeProperty("overflow");
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [bodyFixed]);

  return { setBodyFixed };
};

利用例は以下の通りです。今回は、Next.jsでサポートされているstyled-jsxを使って、モーダルの表示非表示を動的に変更していますが、利用CSSライブラリに応じて適宜書き換えてください。

pages/index.ts
import { useBodyFixed } from "@/lib/common/useBodyFixed";

export const Menu = () => {
  const { bodyFixed, setBodyFixed } = useBodyFixed();
  return (
    <>
      <button onClick={() => setBodyFixed(true)}>
        モーダルウィンドウを開く
      </button>
      <div className="modal">
        <button onClick={() => setBodyFixed(false)}>
          モーダルウィンドウを閉じる
        </button>
      </div>
      <style jsx>{`
        .modal {
          position: fixed;
          top: 0;
          right: 0;
          bottom: 0;
          left: 0;
          display: ${bodyFixed ? "block" : "none"};
        }
      `}</style>
    </>
  );
};

つっかえ棒で背景固定する

下記記事では、また異なる視点で背景がスクロールできないようにする方法が紹介されています。上記より簡潔に実装できるため、試してみるのもアリかと思います。筆者の着眼点がスゴい。
https://qiita.com/yowatsuyoengineer/items/b43b64e1419fa285b758

モーダル内を下までスクロールできない問題の解決

iOSによるモーダルウィンドウの表示に関する問題は、背景固定方法がややこしいだけではありません。iOSの場合、下記実装では、全画面で表示させたいモーダル内の最下部にある項目までスクロールできませんでした。

<div class="modal">
  ここにモーダルで表示させたい要素が入ります。
</div>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%
  height: 100vh;
  overflow-y: auto;
}

モーダルを全画面表示にすることを目的にheight: 100vhを設定すると、iPhoneのアドレスバーの表示・非表示によって動的に変化するビューポートの高さを超えてしまうことが原因です。

この問題を解決するためにはCSSでの実装を工夫したり、JavaScriptで画面の高さを動的に取得する必要がありました。しかし現在では、CSSに1行記述を追加するだけで解決できます。

.modal {
  height: 100vh;
+ height: 100dvh;
  overflow-y: auto;
}

モーダル要素にheight: 100dvhを設定することで、動的に変化するビューポートの高さに応じて、モーダルウィンドウの高さも変化するようになります。結果的に、下までスクロールできない問題が解決できます。神です。

ただし、このdvhという単位も、iOS15.4未満では対応していないことに注意してください。未対応の端末で見られることを想定して、height: 100vhはそのまま設定しておくことで、せめて高さは画面いっぱいに表示されるように設定しておきましょう。

iOS15.4未満を考慮する場合

以下のようにCSSを変更してみてください。モーダルの幅や高さを設定せず、親要素からの相対位置に設定することで、ビューポートの動的なサイズ変更にも対応できるようになります。

.modal {
  position: fixed;
  top: 0;
  left: 0;
- width: 100%
- height: 100vh;
+ bottom: 0;
+ right: 0;
  overflow-y: auto;
}

ただし、本方法はposition: fixedの設定が必要になるため、モーダル以外で全画面表示させたい要素がある場合は、下記記事で紹介されているようなウィンドウの高さをJavaScriptで動的に取得する方法などをオススメします。
https://zenn.dev/tak_dcxi/articles/2ac77656aa94c2cd40bf

今回は以上になります。

ここまで読んでいただき、ありがとうございます🙏

Discussion

ログインするとコメントできます