目次
モーダルの実装は、背景を暗くしてパネルを表示すれば完成するわけではありません。開いた直後にどこへ入力できるか、Tabキーで背景へ抜けないか、閉じた後に元の作業へ戻れるかまで含めて一つの操作です。HTMLのdialog要素を使うと、モーダル表示の基盤をブラウザーへ任せ、アプリ側は内容と状態管理へ集中できます。
本稿は2026年10月24日に仕様を確認した実装解説です。小さな表示設定フォームを題材に、ライブラリーなしの構成を示します。実際の製品へ入れる前には、対象ブラウザーと支援技術での確認が必要です。
スポンサーリンク
showModalとopen属性の違いを押さえる
dialogへopen属性を付けるだけの場合と、show()で開く場合は非モーダルです。背景の操作を止める用途ではshowModal()を呼びます。モーダルはトップレイヤーへ表示され、同じ文書内の背後の要素は操作不能になります。大きなz-indexで既存の重なり順へ割り込む方法とは仕組みが異なります。
仕様上の動きはWHATWG HTML Standardのdialog要素で確認できます。閉じるときはclose()やmethod="dialog"を使い、open属性を直接削除して状態を操作しないようにします。
表示設定フォームの最小構成
<button type="button" id="open-settings">表示設定を開く</button>
<dialog id="settings" aria-labelledby="settings-title">
<form method="dialog">
<h2 id="settings-title">表示設定</h2>
<label>表示名 <input name="displayName" autofocus></label>
<button value="cancel" formnovalidate>キャンセル</button>
<button value="apply">適用</button>
</form>
</dialog>
<p id="result" role="status"></p>
<script>
const opener = document.querySelector('#open-settings');
const dialog = document.querySelector('#settings');
const result = document.querySelector('#result');
opener.addEventListener('click', () => {
dialog.returnValue = '';
if (!dialog.open) dialog.showModal();
});
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'apply') {
const name = dialog.querySelector('[name="displayName"]').value;
result.textContent = '表示名を適用しました:' + name;
}
});
</script>
formのmethod="dialog"は、通常の送信先へデータを送る代わりにダイアログを閉じ、押した送信ボタンのvalueをreturnValueへ入れます。ここではapplyの場合だけ表示へ反映します。cancelはフォームを閉じる操作です。キャンセルへformnovalidateを付けると、後から必須入力を追加しても、入力不備を理由にキャンセルが止まることを避けられます。
開くたびにreturnValueを空文字へ戻すのは、前回の適用結果を次回のEscape操作へ持ち越さないためです。Escapeで閉じた場合に古いapplyが残ると、キャンセルしたつもりの内容を反映してしまいます。閉じた理由と、現在入力されている値は別に扱ってください。
この例の「適用」はページ内の文字列更新だけです。サーバー保存や設定の永続化は行いません。非同期保存が必要なら通常のsubmitを捕捉して検証・保存し、成功を確認してからclose("apply")へ進む構成に変えます。処理中の再送と、失敗時に入力を残す動きも追加します。
スポンサーリンク
最初にフォーカスする場所を決める
例では表示名入力へautofocusを付けています。開いた後、利用者がすぐ編集できる配置です。削除のような取り消しにくい操作なら、最初のフォーカスをキャンセルへ置く方が適切な場合があります。すべてのモーダルで先頭のボタンを選ぶのではなく、操作の目的から決めましょう。
長い規約や複数段落を読ませる場合は、冒頭の見出しへtabindex="-1"を付け、showModal()の後でその見出しへfocus()する方法があります。文章全体をaria-describedbyへ詰めると読み上げが長くなるため、見出しと内容の構造で読めるようにします。こうした判断はW3C WAIのモーダルダイアログ設計指針も参考になります。
見た目と小さい画面への対応
dialog {
box-sizing: border-box;
inline-size: min(32rem, calc(100% - 2rem));
max-block-size: calc(100dvh - 2rem);
overflow: auto;
border: 1px solid #667085;
border-radius: .75rem;
padding: 1.5rem;
}
dialog::backdrop { background: rgb(15 23 42 / .55); }
dialog :focus-visible { outline: 3px solid #175cd3; outline-offset: 3px; }
dialog自体へdisplay:gridなどを常時指定すると、閉じているときの非表示を上書きするおそれがあります。レイアウトは内部のformへ指定するか、dialog[open]へ限定してください。高さを固定せず、狭い画面や拡大表示で必要なときだけスクロールさせます。背景クリックによる閉鎖はこの例では実装していません。見えるキャンセルボタンとEscapeを用意した構成です。
スポンサーリンク
閉じた後の戻り先を確認する
通常はブラウザーが開く前のフォーカスへ戻します。上の例なら「表示設定を開く」ボタンです。SPAで開くボタンを削除したり、保存によって一覧を再描画したりすると、戻り先が存在しない場合があります。その場合に限って、closeイベントで新しく作られた行や次の操作ボタンなど、作業が続く位置へフォーカスを移します。
独自のTab循環処理をネイティブdialogへ重ねると、ブラウザーの動きと衝突しやすくなります。まず標準の挙動を試し、追加処理は再現できる問題に限定します。iframe内で開いたdialogは、親文書全体を操作不能にするものではない点にも注意してください。
公開前に試す操作
- マウスを使わずボタンへ移動し、Enterで開く。表示名へフォーカスが入るか確認する。
- TabとShift+Tabを繰り返し、背景のリンクへ移らないか確認する。
- 適用後に再び開き、入力を変更してEscapeで閉じる。前の適用が再実行されないか確認する。
- キャンセルボタンで閉じ、開いたボタンへ戻れるか確認する。
- 200%の拡大表示とスマートフォンのソフトキーボード表示で、操作ボタンへ到達できるか確認する。
既存のjQueryによるモーダルウィンドウの基本例は、独自オーバーレイを表示する構成です。ネイティブdialogへ移す場合は、背景用要素、独自の表示切り替え、フォーカス制御の責任範囲を見直します。単純にdivをdialogへ置き換えるだけで済むとは考えず、開く・入力する・閉じる・戻るの一連の操作で移行を確かめてください。









