モーダルウィンドウ

簡易的なモーダルウィンドウを実装する方法をご紹介します。
ご紹介するコードは次のような特徴があります。

  • リンク要素から対象(href属性に一致)のモーダルウィンドウを表示
  • Escキーによるモーダルウィンドウの非表示に対応
  • モーダルウィンドウへのフォーカス移動
  • モーダルウィンドウを表示したときにbody要素に"show-modal"クラスを付与
  • aria属性の使用(気持ち)

デモ

Open

サンプルコード

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);
	});
})();

JavaScript逆引きリファレンス一覧へ戻る