dialog要素の改善解説と使い方【HTML】
モーダルを作るなら、自作のdivではなく<dialog>要素を使うのが2026年時点の正解です。フォーカスの閉じ込め、背景の操作無効化、Escキーでのキャンセル、::backdropによる背景の装飾を、ブラウザが標準で面倒を見てくれます。
<dialog>は2022年3月に全モダンブラウザで使えるようになり、現在は Baseline(widely available)です。さらにその後、閉じ方を制御するclosedby属性と、@starting-styleによるアニメーションが加わって、実務で足りない部分がほぼ埋まりました。
この記事では、show()とshowModal()の違いという基本から、実際に詰まるポイント(背景クリックで閉じる、閉じるアニメーション、戻り値の受け取り方)まで解説します。
show() と showModal() は何が違うのか
結論として、ほとんどの場面で使うべきなのはshowModal()です。show()はブラウザが何も守ってくれません。
| 項目 | showModal() |
show() |
|---|---|---|
| 背景の操作 | 不可(自動でinert相当になる) |
可能 |
| フォーカスの閉じ込め | あり | なし |
| Escキーで閉じる | 閉じる(cancelイベント) |
閉じない |
::backdrop |
効く | 効かない |
| ARIA | aria-modal="true" |
aria-modal="false" |
| 重なり順 | トップレイヤー(z-indexの影響を受けない) |
通常のフロー |
show()が向くのは、ページの操作を続けながら参照したいパネル(検索の絞り込み、チャットウィンドウなど)だけです。この場合はEscで閉じる処理を自分で書く必要があります。
非モーダルダイアログの実装
See the Pen
dialog01 by Rin (@rinblog0408)
on CodePen.
<button id="open-dialog">非モーダルダイアログを開く</button>
<dialog id="non-modal-dialog">
<p>非モーダルです。背景のページも操作できます。</p>
<button id="close-dialog" autofocus>閉じる</button>
</dialog>
<script>
const dialog = document.getElementById('non-modal-dialog');
const openButton = document.getElementById('open-dialog');
const closeButton = document.getElementById('close-dialog');
openButton.addEventListener('click', () => {
dialog.show(); // 非モーダルで表示
});
closeButton.addEventListener('click', () => {
dialog.close();
});
// 非モーダルは Esc で閉じないので自分で実装する
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dialog.open) dialog.close();
});
</script>
最後のEscキー処理が非モーダルでは必須です。これが無いと、キーボードだけで操作しているユーザーが閉じられません。
モーダルダイアログの実装
See the Pen
dialog02 by Rin (@rinblog0408)
on CodePen.
<button id="open-modal-dialog">モーダルダイアログを開く</button>
<dialog id="modal-dialog">
<h2>確認</h2>
<p>モーダルです。閉じるまで背景は操作できません。</p>
<button id="close-modal-dialog" autofocus>閉じる</button>
</dialog>
<script>
const modalDialog = document.getElementById('modal-dialog');
document.getElementById('open-modal-dialog')
.addEventListener('click', () => modalDialog.showModal());
document.getElementById('close-modal-dialog')
.addEventListener('click', () => modalDialog.close());
</script>
autofocusを付けている点に注目してください。付けないとダイアログ内の最初のフォーカス可能要素に自動で当たります。「削除する」のような破壊的なボタンが先頭にある場合は、必ず安全な側(キャンセル)にautofocusを置いてください。Enterキーの連打で誤操作が起きます。
なお、<dialog>自体にtabindexを付けてはいけません。ダイアログの枠は操作対象ではないためです。
背景クリックで閉じるには(closedby属性)
「ダイアログの外側をクリックしたら閉じる」は最も要望の多い挙動ですが、既定では閉じません。これを1行で実現するのが closedby 属性です。
<dialog id="dlg" closedby="any">
<p>外側クリックでもEscでも閉じます</p>
<button autofocus>閉じる</button>
</dialog>
| 値 | 閉じ方 |
|---|---|
none |
ボタンやclose()だけ。Escでも閉じない |
closerequest |
Esc+開発者が用意した手段(showModal()の既定) |
any |
外側クリック(light dismiss)+Esc+開発者の手段 |
2026年9月時点で、closedbyは Chrome / Edge / Firefox が対応済み、Safari が未対応です(Interop 2026 の対象項目)。したがって、当面はフォールバックを併記するのが安全です。
const dlg = document.getElementById('dlg');
// closedby 非対応ブラウザ向けのフォールバック
if (!('closedBy' in dlg)) {
dlg.addEventListener('click', (e) => {
// ダイアログ自身(=背景部分)がクリック対象ならば外側クリック
if (e.target === dlg) dlg.close();
});
}
この判定が成り立つのは、<dialog>のボックスの外側(backdrop領域)のクリックが<dialog>自身のイベントとして届くためです。中身を<div>で1枚包んでおくと、内側クリックが誤って閉じる事故を防げます。
逆に、入力途中の内容を守りたいフォームでは closedby="none" にして、明示的な「キャンセル」ボタン以外では閉じないようにする使い方もできます。
フォームの結果を受け取る(method=”dialog”)
<dialog>の中のフォームにmethod="dialog"を指定すると、送信せずにダイアログを閉じ、押されたボタンのvalueがreturnValueに入ります。確認ダイアログの実装がとても短くなります。
<dialog id="confirm-dialog">
<form method="dialog">
<p>この投稿を削除しますか?</p>
<button value="cancel" autofocus>キャンセル</button>
<button value="delete">削除する</button>
</form>
</dialog>
const confirmDialog = document.getElementById('confirm-dialog');
confirmDialog.addEventListener('close', () => {
if (confirmDialog.returnValue === 'delete') {
deletePost();
}
});
confirmDialog.showModal();
イベントは2種類あります。使い分けを間違えると閉じたのに処理が走らない、という不具合になります。
close:閉じたとき必ず発火する。returnValueを読むのはこちらcancel:Escで閉じられたときに発火する。returnValueは更新されない
Escで閉じた場合、returnValueは空文字のままです。上のコードのように「特定の値のときだけ実行する」書き方にしておけば、Escキャンセルも自然に安全側へ倒れます。
開閉をアニメーションさせる
<dialog>はdisplay: noneとdisplay: blockを行き来するため、素直にtransitionを書いても効きません。@starting-styleとtransition-behavior: allow-discreteを使います。
dialog {
opacity: 0;
transform: translateY(-16px);
transition:
opacity .3s ease-out,
transform .3s ease-out,
overlay .3s ease-out allow-discrete,
display .3s ease-out allow-discrete;
}
/* 開いている状態 */
dialog:open {
opacity: 1;
transform: translateY(0);
}
/* 開く瞬間の開始値 */
@starting-style {
dialog:open {
opacity: 0;
transform: translateY(-16px);
}
}
/* 背景も一緒にフェードさせる */
dialog::backdrop {
background: rgb(0 0 0 / 0);
transition: background .3s ease-out, overlay .3s ease-out allow-discrete, display .3s ease-out allow-discrete;
}
dialog:open::backdrop {
background: rgb(0 0 0 / .5);
}
@starting-style {
dialog:open::backdrop { background: rgb(0 0 0 / 0); }
}
@media (prefers-reduced-motion: reduce) {
dialog, dialog::backdrop { transition: none; }
}
ポイントは3つです。
displayとoverlayをtransitionに含め、allow-discreteを付ける:これが無いと閉じるアニメーションが再生されずに即消えます@starting-styleで開始値を書く:開くときのアニメーションはこれが無いと効きませんprefers-reduced-motionを必ず用意する
スクロール・背景まわりでよくある問題
モーダルを開いている間、背景のページがスクロールしてしまうことがあります。showModal()は背景の操作を止めますが、スクロールは止めません。
/* モーダル表示中は背景のスクロールを止める */
body:has(dialog[open]) {
overflow: hidden;
}
また、ダイアログ内が長くなる場合は、ダイアログ自身に高さの上限とスクロールを与えます。
dialog {
max-width: min(90vw, 640px);
max-height: 85dvh;
overflow: auto;
border: none;
border-radius: 12px;
padding: 24px;
}
高さの単位にdvhを使うと、スマートフォンのアドレスバーの伸縮で下部が切れる問題を避けられます。
まとめ
- モーダルは
showModal()。show()はフォーカス閉じ込めもEscも::backdropも無い autofocusは安全な側のボタンに置く。<dialog>本体にtabindexは付けない- 外側クリックで閉じるなら
closedby="any"。Safari未対応なのでe.target === dialogのフォールバックを併記 - 結果の受け取りは
<form method="dialog">+returnValue。closeとcancelの違いに注意 - アニメーションは
@starting-style+transition-behavior: allow-discrete(displayとoverlayを含める) - 背景のスクロールは
body:has(dialog[open]) { overflow: hidden; }で止める
自作モーダルで一番作り込みが必要なのはフォーカス管理とEsc対応ですが、<dialog>はそこを丸ごと引き受けてくれます。CSSの:has()セレクターと組み合わせると、開閉状態に応じたスタイル制御もJavaScript無しで書けます。