JavaScript

素のJavaScriptでモーダルウィンドウ(ポップアップ/ダイアログ/ライトボックス)

jQueryも他のライブラリもなしで、素のJavaScriptだけでモーダルウィンドウを表示する方法をご紹介します。

jQueryの利用有無でLighthouseのパフォーマンス点数が10点ぐらい違うという報告も見ています。ホームページレベルでjQueryに頼りたくなるものと言えば、私はアコーディオンメニューぐらいかなと思っていますが、アコーディオンメニューもjQueryなしで実装する方法をご紹介していますので、そちらもよろしければご参照ください。

モーダルウィンドウとは

「モーダルウィンドウ」は、元の画面の上に表示され、表示されている間、元の画面が操作できない状態になるものを指します。

「ポップアップ」「ダイアログ」「ライトボックス」という言葉があります。
「ポップアップ」と呼ぶものは本記事でご紹介するものとは少し違い、元の画面も操作ができる状態のことを指すのが一般的です。
「ダイアログ」というのは、「ポップアップ」も、「モーダルウィンドウ」も、どちらにも当てはまるような言葉だと思います。
「モーダルウィンドウ」を簡単に作れる「ライトボックス」というライブラリがあり、それにより、「モーダルウィンドウ」のことを「ライトボックス」と呼ぶのが一般化していた時期もあったと思います。

デモ1「モーダルウィンドウ内がスクロールする」

次のボタンを押すとモーダルウィンドウが開きます。モーダルウィンドウ内がスクロールします。

デモ2「サムネイル表示」

次のサムネイルからどれか一つクリックするとモーダルウィンドウが表示されます。

京都 横浜 吉祥寺 北海道 沖縄

デモ1のコード

HTML

<div class="modal-btn-wrapper">
   <button id="modal-btn" class="modal demo">モーダルウインドウを開く</button>
</div>
<div class="modal-overlay">
   <div class="modal-window">
      <button id="close" aria-label="閉じる"></button>
      <h2>モーダルウインドウ</h2>
      <p>省略</p>
   </div>
</div>

CSS

.modal-overlay{
   visibility: hidden;
   position: fixed;
   left: 0;
   top: 0;
   width: 100vw;
   height: 100vh;
   background-color: rgba(0,0,0,.7);
   z-index: 999;
  transition: .3s;
  opacity:0;
}
.modal-overlay.active{
   visibility: visible;
   transition: .3s;
   opacity: 1;
}
.modal-window{
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%,-50%);
   background-color: #fff;
   width: 80%;
   max-width: 900px;
   height: 500px;
   margin: 0;
   padding: 30px;
   overflow-y: auto;
}
#close-01 {
    position: absolute;
    width: 20px;
    height: 20px;
    top: 20px;
    right: 20px;
}
#close-01::before {
   content: '';
   position: absolute;
   top: 50%;
   left: 50%;
   width: 25px;
   height: 1px;
   background-color: #000;
   transform: translate(-50%, -50%) rotate(45deg);
}
#close-01::after {
   content: '';
   position: absolute;
   top: 50%;
   left: 50%;
   width: 25px;
   height: 1px;
   background-color: #000;
   transform: translate(-50%, -50%) rotate(-45deg);
}
.modal-opened {
    overflow-y: hidden; /*背景を固定*/
}

JavaScript

const modal_btn = document.getElementById('modal-btn');
const modal_01 = document.querySelector('modal-overlay');
const closeBtn_01 = document.getElementById('close-01')

modal_btn.addEventListener('click', function() {
   modal_01.classList.add('active');
   document.querySelector('body').classList.add('modal-opened'); //body要素にクラスを与える
   closeBtn_01.addEventListener('click', function() {
      modal_01.classList.remove('active');
      document.querySelector('body').classList.remove('modal-opened'); 
   }, false);
   document.addEventListener('keydown', keydown_ivent);
   function keydown_ivent(e) {
      if(e.key == 'Escape'){
         modal.classList.remove('active');
         document.querySelector('body').classList.remove('modal-opened');
      }
   }
}, false);

デモ1の解説

CSSは分かりやすくするため、見た目の調整のための部分は省略し、モーダルウィンドウとしての機能的に意味があるところだけ掲載しています。

デモ1は、以下の仕様となっています。

  1. モーダルウィンドウが表示されている間はその背景となる元々表示されていた画面はスクロールさせない
  2. モーダルウィンドウ内のスクロールは許容する
  3. 「×」ボタン押下だけでなく、Escキーでもモーダルウィンドウを閉じることができる
  • .modal-overlayがモーダルウインドウとなる部分で、最初はvisibility: hiddenで非表示にしておきます。
  • クラス名activeの有無によってvisibilityopacityを変更し、表示/非表示の切り替えを行っています。transitionopacityを徐々に変化するアニメーションを付けています。ここはdisplay: noneだとアニメーションが効きません。
  • モーダルウインドウはoverflow-y: autoでスクロールされるようにし、モーダルウィンドウの背景は、モーダルが開いているときだけbody要素をoverflow-y: hiddenでスクロールしないようにしています。ここは気を付けないと、タブレットやスマホでの操作に支障があります。
  • JavaScriptの12~18行目がEscキーを押下してモーダルを閉じる挙動の実装です。
    「×」ボタン押下でも閉じられるので、ここはアクセシビリティ上も必須というわけではないですが、要望としていただいたことがあります。私はそういう習慣がないですが、Escキーでモーダルを閉じる、というのが癖づいている人にとっては、Enterキーでボタンが押せないなどと同じくらい困るのだろうとは思いますので、この部分もご紹介しました。

なお、この画面のデモ1は.modal-overlayを「モーダルウインドウを開く」に隣接するところではなく、実際にはheaderタグに隣接して配置しています。そうしないと、position: fixedにしているヘッダー部分などが前面に出てしまうか、完全に隠れてしまうためです。

headerタグに隣接して配置させるために、次のようなコーディングを施しています。

const modal_overlay = document.createElement('div');
let textContent = '<div class="modal-overlay">';
let textContent += '<div class="modal-window">';
let textContent += '<button id="close" aria-label="閉じる"></button>';
let textContent += '<h2>モーダルウインドウ</h2>';
let textContent += '<p>省略</p>';
let textContent += '</div>';
let textContent += '</div>';
modal_overlay.innerHTML = textContent;
const thisHeader = document.querySelector('header');
thisHeader.after(modal_overlay);

document.createElementでdiv要素を作成し、innerHTMLで作成したdiv要素の中にHTMLを格納する、ということをしています。

elementA.after(elementB)は、A要素の後にB要素を挿入するというメソッドです。

デモ2のコード

HTML

<div class="thumbnails-wrapper w-02">
   <img src="/wp-content/themes/bf-official/images/blog/slide-01.jpg" alt="京都">
   <img src="/wp-content/themes/bf-official/images/blog/slide-02.jpg" alt="横浜">
   <img src="/wp-content/themes/bf-official/images/blog/slide-03.jpg" alt="吉祥寺">
   <img src="/wp-content/themes/bf-official/images/blog/slide-09.jpg" alt="北海道">
   <img src="/wp-content/themes/bf-official/images/blog/slide-10.jpg" alt="沖縄">
</div>
<div class="images-overlay o-02">
   <button id="close-02" aria-label="閉じる"></button>
   <div class="images-wrapper w-02">
      <img src="/wp-content/themes/bf-official/images/blog/slide-01.jpg" alt="京都">
      <img src="/wp-content/themes/bf-official/images/blog/slide-02.jpg" alt="横浜">
      <img src="/wp-content/themes/bf-official/images/blog/slide-03.jpg" alt="吉祥寺">
      <img src="/wp-content/themes/bf-official/images/blog/slide-09.jpg" alt="北海道">
      <img src="/wp-content/themes/bf-official/images/blog/slide-10.jpg" alt="沖縄">
   </div>
</div>

CSS

.thumbnails-wrapper{
   display: flex;
   flex-wrap: wrap;
   justify-content: center;
   gap: 20px;
   margin: 50px 0;
}
.thumbnails-wrapper img{
   max-width: 140px;
   object-fit: cover;
}
.thumbnails-wrapper img:hover{
   opacity: .7;
   cursor: pointer;
}
.images-overlay{
   display: none;
}
.images-wrapper {
    padding: 0 20px;
    max-width: 1000px;
}
.images-overlay{
   display: flex;
   align-items: center;
   justify-content: center;
   position: fixed;
   left: 0;
   top: 0;
   margin: 0 calc(50% - 50vw);
   width: 100vw;
   height: 100vh;
   background-color: rgba(0, 0, 0, .8);
   visibility: hidden;
   opacity: 0;
   z-index: 1003;
   transition: .3s;
}
.images-overlay.active{
   visibility: visible;
   opacity: 1;
   transition: .3s;
}
.images-overlay img{
   display:none;
}
.images-overlay img.active{
   display: block;
}
#close-02 {
   position: absolute;
   width: 50px;
   height: 50px;
   top: 10%;
   right: 5%;
   background-color: rgba(0, 0, 0 , .8);
   transition: .3s;
}
#close-02:hover {
   opacity: .7;
   transition: .3s;
}
#close-02::before {
   content: "";
   position: absolute;
   top: 50%;
   left: 50%;
   width: 30px;
   height: 2px;
   background-color: #fff;
   transform: translate(-50%, -50%) rotate(45deg);
}
#close-02::after {
   content: "";
   position: absolute;
   top: 50%;
   left: 50%;
   width: 30px;
   height: 2px;
   background-color: #fff;
   transform: translate(-50%, -50%) rotate(-45deg);
}

JavaScript

//画像を全て取得
const images_02 = document.querySelectorAll('.thumbnails-wrapper.w-02 img');
//モーダル外枠取得
const modal_02 = document.querySelector('.images-overlay.o-02');
//閉じるボタン取得
const closeBtn_02 = document.getElementById('close-02')
images_02.forEach(function( image , index) {
   image.addEventListener('click', function() {
      //モーダルが表示されたらbodyをスクロール不可にするためクラス名を付与
      document.querySelector('body').classList.add('modal-opened');
      //indexは配列で0から始まるため、1を足すことでクリックした画像の順番と一致させる
      let i = index + 1;
      //クリックした画像を取得
      let thisImg = document.querySelector('.images-wrapper.w-02 img:nth-of-type(' + i + ')');
      //モーダルを表示するためクラス名を付与
      modal_02.classList.add('active');
      //クリックした画像を表示するためクラス名を付与
      thisImg.classList.add('active');
      closeBtn_02.addEventListener('click', function() {
         //モーダルを非表示にするためクラス名を削除
         modal_02.classList.remove('active');
         //クリックした画像を非表示にするためクラス名を削除
         thisImg.classList.remove('active');
         //bodyをスクロール可能に戻すためクラス名を削除
         document.querySelector('body').classList.remove('modal-opened'); 
      }, false);
   }, false);     
});

デモ2の解説

  • .images-overlayがモーダルウィンドウとなる部分で、最初はvisibility: hiddenで非表示にしておきます。
  • サムネイルの一つをクリックしたときに、そのクリックした画像がどれかを把握するため、document.querySelectorAllで取得した画像をforEachで回しつつindexに1を足すことで何番目の画像であるかを特定します。indexは0から始まるため、1を足すことで画像の順番と一致します。
  • あとはデモ1と同じです。

アクセシビリティとSEOに関する留意点

visibility: hiddenは画面上に表示しませんし、スクリーンリーダーも無視します。

モーダルウィンドウ内に説明すべき内容がある場合、アクセシビリティのことだけで言えば、クリックすると表示される内容がある、ということが伝わるようになっていれば問題というわけでもありません。開くボタンの近くに説明文を書くとか、開くボタン自体にラベルを付けるなどすればよいです。

しかし、SEOのことを考えると、ご紹介しておいてなんですが、デモ1のようにモーダルウィンドウ内に文章があるという状態は避けた方が良いと言えます。何故ならばCSSで非表示にしている場合、その中身はないページという前提で、Googleはそのページを評価する可能性があるからです。

よってモーダルウインドウは、デモ2のようのサムネイルをクリックしたら画像が大きく表示されるようにするといった使い方が一般的だと思います。

おまけ(バグあり)デモ「モーダルウィンドウ内で前後の画像を表示する」

次のサムネイルからどれか一つクリックするとモーダルウィンドウが表示されます。かつ「<」「>」のボタンが表示され、前後の画像にモーダル内で移動できます。ただし、タイトルの通りバグがあります。「×」ボタンを1度押して、再度どれかサムネイルを選び、「<」「>」を押す、という操作を何度か繰り返してみてください。そのうち、画像が2枚以上表示されると思います。なお、粘り強く操作しないとなかなか出くわさないかもしれません。

京都 横浜 吉祥寺 北海道 沖縄

おまけ(バグあり)デモのコード

HTML

<div class="thumbnails-wrapper w-03">
   <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-01.jpg" alt="京都">
   <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-02.jpg" alt="横浜">
   <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-03.jpg" alt="吉祥寺">
   <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-09.jpg" alt="北海道">
   <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-10.jpg" alt="沖縄">
</div>
<div class="images-overlay o-03">
   <button id="close-03" aria-label="閉じる"></button>
   <div class="images-wrapper w-03">
      <button id="prev" aria-label="前へ"></button>
      <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-01.jpg" alt="京都">
      <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-02.jpg" alt="横浜">
      <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-03.jpg" alt="吉祥寺">
      <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-09.jpg" alt="北海道">
      <img decoding="async" src="/wp-content/themes/bf-official/images/blog/slide-10.jpg" alt="沖縄">
      <button id="next" aria-label="次へ"></button>
   </div>
</div>

CSS

CSSはデモ2とほとんど内容が同じなので、そのままの掲載は省略させていただきます。
.images-wrapperposition: relativeにして、前ボタン、次ボタンはposition: absoluteにし、位置を調整しています。

JavaScript

<script>
//画像を全て取得
const images_03 = document.querySelectorAll('.thumbnails-wrapper.w-03 img');
//モーダル外枠取得
const modal_03 = document.querySelector('.images-overlay.o-03');
//閉じるボタン取得
const closeBtn_03 = document.getElementById('close-03')
const length = images_03.length;
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
images_03.forEach(function( image , index) {
   image.addEventListener('click', function() {
      //モーダルが表示されたらbodyをスクロール不可にするためクラス名を付与
      document.querySelector('body').classList.add('modal-opened');
      //indexは配列で0から始まるため、1を足すことでクリックした画像の順番と一致させる
      let i = index + 1;
      //クリックした画像を取得
      let thisImg = document.querySelector('.images-wrapper.w-03 img:nth-of-type(' + i + ')');
      //モーダルを表示するためクラス名を付与
      modal_03.classList.add('active');
      //クリックした画像を表示するためクラス名を付与
      thisImg.classList.add('active');
      if (i > 1){
         prevBtn.classList.add('active');
      }else{
         if(prevBtn.classList.contains('active')){
            prevBtn.classList.remove('active');
         }
      }
      if (i < length){
         nextBtn.classList.add('active');
      }else{
         if(nextBtn.classList.contains('active')){
            nextBtn.classList.remove('active');
         }
      }
      //前へボタンクリック     
      prevBtn.removeEventListener('click', prev);
      prevBtn.addEventListener('click', prev);
      function prev() {
         //最初の画像の時は「>」ボタンを非活性に
         if (i > 1){
            i = i - 1;
            if (i == 1){
               prevBtn.classList.remove('active')
            }
            if (i == (length - 1)){
               nextBtn.classList.add('active')
            }
            thisImg.classList.remove('active');
            thisImg = document.querySelector('.images-wrapper.w-03 img:nth-of-type(' + i + ')');
            thisImg.classList.add('active');
         }
      }
      //次へボタンクリック
      nextBtn.removeEventListener('click', next);
      nextBtn.addEventListener('click', next);
      function next() {
         //最後の画像の時は「<」ボタンを非活性に
         if (i < length){
            i = i + 1;
            if (i == length){
               nextBtn.classList.remove('active')
            }
            if (i == 2){
               prevBtn.classList.add('active')
            }
            thisImg.classList.remove('active');
            thisImg = document.querySelector('.images-wrapper.w-03 img:nth-of-type(' + i + ')');
            thisImg.classList.add('active');
         }
      }
      //閉じるボタンクリック
      closeBtn_03.addEventListener('click', function() {
         //モーダルを非表示にするためクラス名を削除
         modal_03.classList.remove('active');
         //クリックした画像を非表示にするためクラス名を削除
         thisImg.classList.remove('active');
         //bodyをスクロール可能に戻すためクラス名を削除
         document.querySelector('body').classList.remove('modal-opened'); 
      }, false);
   }, false);     
});
</script>

おまけデモ解説

「<」「>」ボタンを押すと、その時表示されている画像(変数名thisImg)のクラス名からactiveを削除。ループで回しているので、i + 1 または i - 1を計算することで次に表示する画像を特定。その画像にactiveのクラス名を追加するという処理をしています。

ただ上述の通り、大体思った通りに動くものの、このソースコード自体は操作しているうちにバグが生じます。addEventListenerの前にremoveEventListenerを入れているのはそのバグ回避のためで、これを入れることで、多少症状が緩和されてはいます。

なのにこれ、何故紹介しているかというとわかりますよね?

バグの解決策、または原因だけでもいいので、分かる方ご連絡ください。

よろしくお願いします!

まとめ

モーダルウィンドウをjQueryなしで実装する方法とSEOを考えたうえでの留意点をご紹介しました。

著者のイメージ画像

BringFlower
稲田 高洋(Takahiro Inada)

2003年から大手総合電機メーカーでUXデザインプロセスの研究、実践。UXデザイン専門家の育成プログラム開発。SEOにおいても重要なW3Cが定めるWeb標準仕様策定にウェブアクセシビリティの専門家として関わる。2010~2018年に人間中心設計専門家を保有、数年間ウェブアクセシビリティ基盤委員も務める。その後、不動産会社向けにSaaSを提供する企業の事業開発部で複数サービスを企画、ローンチ。CMSを提供し1000以上のサイトを分析。顧客サポート、サイト運営にも関わる。
2022年3月にBringFlowerを開業し、SEOコンサル、デザイン、ウェブ制作を一手に受ける。グッドデザイン賞4件、ドイツユニバーサルデザイン賞2件、米国IDEA賞1件の受賞歴あり。