モーダルウィンドウ
簡易的なモーダルウィンドウを実装する方法をご紹介します。
ご紹介するコードは次のような特徴があります。
- リンク要素から対象(href属性に一致)のモーダルウィンドウを表示
- Escキーによるモーダルウィンドウの非表示に対応
- モーダルウィンドウへのフォーカス移動
- モーダルウィンドウを表示したときにbody要素に"show-modal"クラスを付与
- aria属性の使用(気持ち)
デモ
サンプルコード
HTML
<!-- トリガーとなるリンク要素 -->
<p><a href="#modal" class="modal-window-trigger">Open</a></p>
<!-- モーダルウィンドウ本体 -->
<div class="modal-window" id="modal" aria-labelledby="modal-title" aria-hidden="true">
<div class="content" tabindex="-1">
<div class="header" id="modal-title">Title</div>
<div class="body">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vero laborum eaque rerum exercitationem assumenda accusamus in ipsam quos dolor aut mollitia dolorum, pariatur eligendi recusandae? Dignissimos blanditiis consequatur illo dolorum?</p>
</div>
<div class="footer">
<div class="controls">
<button type="button" class="ok">OK</button>
<button type="button" class="cancel">Cancel</button>
</div>
</div>
<button type="button" class="close">Close</button>
</div>
</div>
CSS
.modal-window {
position:fixed;
top:0;
left:0;
z-index:100;
display:flex;
justify-content:center;
align-items:center;
width:100vw;
height:100vh;
background-color:rgba(0, 0, 0, 0.7);
backdrop-filter:blur(5px);
}
.modal-window[aria-hidden="true"] {
display:none;
}
/* 本体 */
.modal-window .content {
position:relative;
box-sizing:border-box;
margin:20px;
padding:20px;
max-width:600px;
background-color:#fff;
}
/* ヘッダー */
.modal-window .content .header {
font-size:2.4rem;
font-weight:bold;
}
/* コンテンツ */
.modal-window .content .body {
margin-top:20px;
}
/* フッター */
.modal-window .content .footer {
display:flex;
justify-content:space-between;
margin-top:20px;
}
/* 制御ボタン */
.modal-window .content .footer .controls {
margin-left:auto;
}
.modal-window .content .footer .controls button {
padding:calc(0.8rem + 0.16em) 1.6rem 0.8rem;
background-color:#333;
font-size:1.6rem;
color:#fff;
transition:background-color 0.2s;
}
.modal-window .content .footer .controls button:nth-child(n+2) {
margin-left:10px;
}
.modal-window .content .footer .controls button:hover,
.modal-window .content .footer .controls button:focus {
background-color:#444;
}
/* 閉じるボタン */
.modal-window .content .close {
position:absolute;
top:-10px;
right:-10px;
border-radius:50%;
width:40px;
height:40px;
background:linear-gradient(#fff, #fff) 50% 50% / 3px 66% no-repeat, #333 linear-gradient(#fff, #fff) 50% 50% / 66% 3px no-repeat;
font-size:0;
transform:rotate(45deg);
transition:background-color 0.2s;
}
.modal-window .content .close:hover,
.modal-window .content .close:focus {
background-color:#444;
}
JavaScript
(function() {
window.addEventListener('DOMContentLoaded', function() {
// 開くためのリンク要素を取得
var linkElems = document.querySelectorAll('.modal-window-trigger'),
bodyElem = document.body;
if (!linkElems.length) return;
var modalElem, stockfocusElem;
/**
* モーダルウィンドウの表示切り替え
* @param {boolean} isShow 表示するかどうか
*/
var changeShowModal = function(isShow) {
modalElem.setAttribute('aria-hidden', isShow ? 'false' : 'true');
modalElem.querySelector('.content').setAttribute('tabindex', isShow ? 0 : -1);
};
/**
* モーダルウィンドウを閉じる
*/
var closeModal = function() {
if (!modalElem || !bodyElem.classList.contains('show-modal')) return;
bodyElem.classList.remove('show-modal');
changeShowModal(false);
stockfocusElem.focus();
modalElem = stockfocusElem = null;
};
// モーダル表示用リンクを繰り返す
Array.prototype.forEach.call(linkElems, function(elem) {
modalElem = document.querySelector(elem.getAttribute('href'));
if (!modalElem) return true;
// モーダル表示用リンクを押した時
elem.addEventListener('click', function(event) {
event.preventDefault();
if (bodyElem.classList.contains('show-modal')) return;
modalElem = document.querySelector(event.target.getAttribute('href'));
stockfocusElem = document.activeElement;
bodyElem.classList.add('show-modal');
changeShowModal(true);
modalElem.querySelector('.content').focus();
});
if (modalElem.classList.contains('ready')) return true;
// タブ操作で最初と最後の繰り返し
var anchorElem = document.createElement('a');
anchorElem.setAttribute('tabindex', -1);
anchorElem.classList.add('focus-tmp');
anchorElem.style.pointerEvents = 'none';
modalElem.insertAdjacentElement('afterbegin', anchorElem.cloneNode());
modalElem.appendChild(anchorElem);
// オーバーレイを押した時
modalElem.addEventListener('click', function(event) {
if (event.target !== modalElem) return;
closeModal();
}, false);
// 閉じるボタンを押した時
modalElem.querySelector('.close').addEventListener('click', function() {
closeModal();
}, false);
// OKボタンを押した時
modalElem.querySelector('.ok').addEventListener('click', function() {
alert('OK');
closeModal();
}, false);
// Cancelボタンを押した時
modalElem.querySelector('.cancel').addEventListener('click', function() {
alert('Cancel');
closeModal();
}, false);
modalElem.classList.add('ready');
});
// Escキーを押した時
window.addEventListener('keydown', function(event) {
if (event.keyCode === 27) closeModal();
if (event.keyCode === 9) {
var focusTmpElem = modalElem.querySelectorAll('.focus-tmp');
if (event.shiftKey) {
if (document.activeElement === modalElem.querySelector('iframe')) {
focusTmpElem[1].focus();
} else if (document.activeElement === modalElem.querySelector('.content')) {
focusTmpElem[1].focus();
}
} else {
if (document.activeElement === modalElem.querySelector('.close')) {
if (modalElem.querySelector('iframe')) {
modalElem.querySelector('.content').focus();
} else {
focusTmpElem[0].focus();
}
}
}
}
}, false);
});
})();