※このページはプロモーションを含みます
モーダルウインドウ機能を実装するのに古い方法で作成していませんか?
古い方法とは次のようなやり方です。
- オーバーレイ(モーダルの背景)用のdivタグを用意
- モーダルの実体用のdivタグを用意
- cssもしくはjsでpositionやopacity、displayなどの値を操作
- 開閉イベントを登録
- モーダルがアクティブ状態の時に背景がスクロールできないようにする
この記事はdialogというHTMLタグを利用したモーダルウインドウの実装方法になります。
2022年9月以前はFireFoxが実装していなかったのですがIEの終了とFireFox98から実装されたことによってほぼ現行ブラウザでの使用が可能になっています。
また先述した「古い方法」だと面倒くさい「背景スクロール制御」はcssの新機能:hasでとても簡単に設定可能になりました。:hasの機能と合わせて覚えておくと良いでしょう。
とても簡単なdialogタグですが非表示アニメーションのつけ方にはクセがあります。そこのところは後ろの方に記述します。
▼dialogタグのブラウザ実装状況
https://caniuse.com/?search=dialog
▼:hasスタイルのブラウザ実装状況
https://caniuse.com/?search=has
基本的設定
dialogタグを書く
dialogタグの内部にdivタグとコンテンツを記入します
<button id="opener">開く</button>
<dialog>
<div class="dialog-inner">
<div class="dialog-header">
<button id="closer">閉じる</button>
<h2>見出し</h2>
</div>
<div class="dialog-body">
<p>モーダルウィンドウにいれるコンテンツです</p>
</div>
</dialog>
</dialog>
cssで装飾する
次にモーダルウインドウの見た目を調整していきますが上記のHTMLでは見えないので一時的に表示させます。
dialogタグにopen属性を加えます。
<dialog open>
</dialog>
dialogタグが表示されたらcssを調整していきます。
dialogタグはブラウザのデフォルトスタイルシート(user agent stylesheet)で次のようなスタイルがあたっています
▼FireFoxのデフォルトスタイルシート
dialog {
position: absolute;
display: block;
inset-inline-start: 0;
inset-inline-end: 0;
margin: auto;
border-width: initial;
border-style: solid;
border-color: initial;
border-image: initial;
padding: 1em;
background-color: Canvas;
color: CanvasText;
width: -moz-fit-content;
height: -moz-fit-content;
}
dialog:not([open]) {
display: none;1)
}
dialog:modal {
-moz-top-layer: top !important;
position: fixed;
overflow: auto;
visibility: visible;
inset-block-start: 0;
inset-block-end: 0;
max-width: calc(100% - 6px - 2em);
max-height: calc(100% - 6px - 2em);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.1);
}
dialog::backdropで疑似要素が設定されていてこれが以前の方法でいう「オーバーレイ」にあたるものです。
またposition:absoluteとmargin:autoによって最初から画面中央に表示されるようになっています。
ここからモーダルの見た目を整えていきます。
dialog {
border: none;
padding: 0; /* ★外側クリックでモーダルを閉じる時に重要 */
border-radius: 10px;
box-shadow: 0 0 30px rgb(255 255 255/.5);
&::backdrop {
backdrop-filter: blur(8px);
background-color: rgb(0 0 0/.3);
}
}
.dialog-inner {
padding: 20px; /* ★外側クリックでモーダルを閉じる時に重要 */
}
その他の設定はお好みで調整してください。
ポイントはdialog自体のpaddingを0にして内側のコンテナーdivタグのpaddingで調整するということです。
これは後述する「外側クリックでモーダルを閉じる」時に重要になってきます。
dialogタグのデフォルトスタイルがpaddingを少しとっているのでこれをなくすことによって意図しないクリック判定が起きるのを防ぐことができます。
javascriptで開閉操作する
ようやくjavascriptで開閉機能を追加していきます。
const openBtn = document.getElementById('opener');
const closeBtn = document.getElementById('closer');
const dialog = document.querySelector('dialog');
openBtn.addEventListener('click', () => {
dialog.showModal(); /* show()でも開くがshowModalにすると::backdrop要素がつきよりモーダルっぽい */
});
closeBtn.addEventListener('click', () => {
dialog.close();
});
▼javascriptで利用できるdialogのメソッドは次の3つです
- close()
- show()
- showModal()
▼show()とshowModal()の違い
show()は::backdrop(オーバーレイ)が出現しません。
これに対しshowModal()は::backdrop要素が出現し、ESCキーで閉じることができるようになります。
▼HTMLDialogElement – Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/HTMLDialogElement
これで単純なモーダルウインドウが実装できました。
ただし、このままではまだもの足りないモーダルなので機能を追加してよりよいものにしていきます。
追加機能
モーダルが表示されている時に背景をスクロールさせない
こちらは非常に簡単です。
cssに新たに追加された疑似クラス:has()を使用します。
html:has(dialog[open]) {
overflow: hidden;
}
:has()は指定された要素を持つ親要素のスタイルを指定できる非常に使い勝手のよいものです。
今回のコードの場合は
“open属性がついているdialogタグを子にもつhtmlタグのoverflowをhiddenにする”
という指定を行っています。
▼:has()についてさらに詳しく見るならこちら
:has() – CSS: カスケーディングスタイルシート | MDN
▼:has()のブラウザ対応状況
“:has()” | Can I use… Support tables for HTML5, CSS3, etc
以前は以下のような細かい処理を行っていましたがそれが不要になります!!!
- 開いた時にjsでその時のスクロール位置を保存
- pageに該当するタグのpositionをfixedに固定
- 閉じる時はpageに該当するタグのfixedを解除
- 保存していたスクロール位置を戻す
外側クリックでモーダルを閉じる
まずモーダルウインドウを閉じる関数を新たに用意します
// Before
closeBtn.addEventListener('click', () => {
dialog.close();
});
// After
closeBtn.addEventListener('click', () => {
closeModal();
});
function closeModal() {
dialog.close();
}
そして以下の処理を追加します
- dialogタグ全体にクリックイベントを登録
- イベントのターゲットを判定
- 内側のコンテナーに該当しないもののみ閉じる
closeBtn.addEventListener('click', () => {
closeModal();
});
// Add
dialog.addEventListener('click', (e) => {
if (e.target.closest(".dialog-inner") === null) {
closeModal();
}
});
function closeModal() {
dialog.close();
}
アニメーション付きで開く
モーダルを開くときにアニメーションをつける方法は簡単です。
open属性を持つdialogタグにCSSを設定します。
dialog[open] {
animation-name: modalIn;
animarion-duration: .3s;
animation-fill-mode: fowards;
animation-timing-function: ease-out;
}
@keyframes modalIn {
0% {
transform: translateY(-110%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
transitionプロパティで設定してもよいですが、より複雑な動きに対応できるようにkeyframsアニメーションにしています
▼keyframesアニメーションについて
@keyframes – CSS: カスケーディングスタイルシート | MDN
これで画面上部から現れるアニメーションができました。
アニメーション付きで閉じる
開くアニメーションの追加は簡単でしたが、閉じる方は少し複雑です。
dialogのデフォルトスタイルがdisplay:none;になるためopen属性をなくすと即非表示になってしまうからです。
これを回避するためには次のような流れでjsを設定していくことになります。
- 閉じるイベントでアニメーション用classを追加
- CSSアニメーションの終了イベントでアニメーション用classを削除
- dialogのopen属性を削除(close)
- CSSアニメーションの終了イベントを解除
「外側クリックでモーダルを閉じる」で作成したcloseModal()関数に処理を書いていきます。
- アニメーション用のclassを.hideにします
function closeModal() {
// ①クラスを追加
dialog.classList.add('hide');
// ②アニメションの終了イベント"animationend"を登録
dialog.addEventListener('animationend', function myfn() {
// ②アニメション用classを削除
dialog.classList.remove('hide');
// ③dialogのopen属性を削除
dialog.close();
// ④CSSアニメーション終了イベントを解除
dialog.removeEventListener('animationend', myfn);
});
}
終了イベントを解除する理由
④で終了イベントを解除していますがこれがないとうまく動きません。
全てのアニメーション終了時に閉じる処理が発動されてしまうからです。
もし④がないと開くアニメーションが終わった時点で閉じる処理が発動されてしまいます…
addEventListenerのcallbackに「myfn」という名前をつけている理由
addEventListenerで登録した関数の中でそのイベント自身を解除したいときは関数名を明示しなくてならないからです。
例えば次のようなコードはNGになります。
// NG
dialog.addEventListener('animationend', (event) => {
// イベントを解除したいがこれではダメ
dialog.removeEventListener('animationend', event);
});
myfnという名前は適当なのでほかのなまえでOKです。
javascriptの処理がかけたので次はcssでアニメーションを設定します。
dialog.hide {
animation-name: modalOut;
animation-duration: .3s;
animation-fill-mode: fowards;
animation-timing-function: ease-out;
}
@keyframes modalOut {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-110%);
opacity: 0;
}
}
これで画面上部に消えていくアニメーションが設定できました。
結果コード
今回の最終的なコードはこちらになります。
See the Pen dialog-modal-window by watashi-xyz (@watashi-xyz) on CodePen.
最後に
閉じるアニメーションを加える処理はやや複雑ですがdialogタグを使用することで今までよりもモーダルウインドウの実装が楽になります。
dialogタグを積極的に使って楽をしていきましょう。