HTML <dialog> 要素 実践ガイド ― JSライブラリなしでアクセシブルなモーダルを作る
モーダルを自前で実装するために大量のJSを書いていたあの頃にさよなら。<dialog> 要素なら、フォーカス管理もEscキー対応もブラウザがやってくれます。

モーダル実装、自前で書くと意外としんどい
「モーダル作って」と言われると、軽いタスクに見えて実はけっこう奥が深い、というのは現場で何度も体験することです。背景に半透明レイヤーを重ねて、中央に箱を置いて——ここまではすぐ書ける。問題はそこから先です。Escキーで閉じる、背景クリックで閉じる、開いたときに中の入力欄にフォーカスを当てる、開いている間は背景のリンクにTabキーで飛んでいかないようにする(フォーカストラップ)、閉じた後は元のボタンにフォーカスを戻す。気付くとjQueryプラグインかReactのモーダルライブラリを探している、という流れ、よくありますよね。
実は、これらすべてを <dialog> 要素がブラウザネイティブで面倒見てくれます。showModal() を呼ぶだけで、フォーカストラップもEscキーも動く。スクリーンリーダーにも「ダイアログが開いたよ」と正しく伝わる。HTMLの仕様としてアクセシビリティが組み込まれているのが、この要素の最大の魅力です。
最小構成:showModal() と close() の2つだけ
使い方は驚くほどシンプルです。<dialog> をHTMLに置いて、JSで showModal() を呼ぶ。閉じるときは close()。これだけで、モーダルとして必要な振る舞いが一通り手に入ります。
<button id="openBtn">開く</button>
<dialog id="myDialog">
<h4>お知らせ</h4>
<p>これは <dialog> 要素のモーダルです。</p>
<form method="dialog">
<button>閉じる</button>
</form>
</dialog>
<script>
openBtn.addEventListener('click', () => {
myDialog.showModal();
});
</script>
注目してほしいのが <form method="dialog"> の部分。フォームの送信メソッドに dialog を指定すると、その中のボタンを押しただけでダイアログが閉じます。JSを一行も書かずに「閉じるボタン」が完成する、というのが地味に嬉しいポイントです。
ボタンを押すとダイアログが開きます。Escキー、背景クリック(後述)、閉じるボタン、すべてで閉じることを試してみてください。
ちなみに show() というメソッドもあるのですが、これは「非モーダルダイアログ」になります。背景がクリックできて、フォーカストラップも効きません。モーダルとして使うなら必ず showModal() のほうを呼びましょう。
現場でよく使うパターン:背景クリックで閉じる
<dialog> はEscキーでは閉じてくれますが、背景(バックドロップ)をクリックしたときに閉じる挙動は標準では入っていません。これは現場でほぼ必ず実装することになるので、定番の書き方を覚えておくと便利です。
/* これだと中身をクリックしても閉じてしまう */
dialog.addEventListener('click', () => {
dialog.close();
});
dialog.addEventListener('click', (e) => {
/* e.target が dialog 自体なら、それは背景部分のクリック */
if (e.target === dialog) {
dialog.close();
}
});
ちょっとしたトリックですが、これは <dialog> の特殊な構造を利用しています。ブラウザは「ダイアログの中身(コンテンツ部分)」と「背景部分」を、見た目上は分けて表示しますが、HTML上はどちらも同じ <dialog> 要素のクリックとして扱われます。なので、クリックされた要素が <dialog> そのものなら背景、子要素なら中身、という判定になるわけです。
開いた後、暗くなった背景部分をクリックしてみてください。中身(白い箱)をクリックしても閉じないのがポイント。
応用:フォーム入力+戻り値の受け取り
<dialog> の本領が発揮されるのは、入力フォームと組み合わせたとき。<form method="dialog"> の中でボタンに value 属性を持たせると、どのボタンが押されたかを returnValue で受け取れます。「OK/キャンセル」のような選択を、JSのイベント分岐を書かずに表現できる仕組みです。
<dialog id="nameDialog">
<form method="dialog">
<label>お名前
<input name="username" required>
</label>
<button value="cancel">キャンセル</button>
<button value="ok">送信</button>
</form>
</dialog>
<script>
nameDialog.addEventListener('close', () => {
/* 押されたボタンのvalue が returnValue に入る */
if (nameDialog.returnValue === 'ok') {
/* フォームの値を取り出して処理 */
}
});
</script>
名前を入力して送信ボタンを押すと、下に結果が表示されます。キャンセルだと何も起きません。
キャンセル側のボタンに formnovalidate を付けているのにも理由があります。これがないと、必須項目が空の状態でキャンセルを押そうとしても、HTMLのバリデーションが先に走って閉じられないからです。「キャンセルだけは無条件で通す」というのは現場でよく必要になる調整です。
開閉アニメーションをCSSだけで付ける
標準の <dialog> は開閉が一瞬でパッと切り替わるので、ちょっと素っ気なく感じることがあります。最近のブラウザでは、CSSの @starting-style と transition-behavior(または transition での allow-discrete)を使うと、開閉アニメーションがCSSだけで書けるようになりました。
dialog {
opacity: 0;
transform: translateY(-20px) scale(0.95);
transition: opacity 0.25s, transform 0.25s,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 開きはじめの状態を指定する */
@starting-style {
dialog[open] {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
}
ポイントは @starting-style です。<dialog> は閉じている間 display: none 相当になるので、普通に書くと「最初の状態」がそもそも存在しません。@starting-style はその「表示直前のスタート地点」を定義するための仕組み。これがあるおかげで、フェードイン+スライドのようなアニメーションが成立します。
開くときも閉じるときも、ふわっとフェード+スライドします。背景の透明度もトランジションしているのが分かるはずです。
もう一歩:背景スタイルとアクセシビリティの仕上げ
<dialog> の背景部分(バックドロップ)には、::backdrop 疑似要素でスタイルを当てられます。色の濃さや、ぼかし(backdrop-filter)を調整して、サイトのトーンに馴染ませると一気に印象が変わります。
dialog::backdrop {
background: rgba(15, 15, 30, 0.55);
backdrop-filter: blur(2px);
}
アクセシビリティ面では、デフォルトでロール(role="dialog")が付与されるので明示的な指定は不要です。ただし、ダイアログのタイトルに対応する見出しがある場合は aria-labelledby でその見出しを参照しておくと、スクリーンリーダーの読み上げが自然になります。説明文に対しては aria-describedby です。
<dialog aria-labelledby="dlgTitle" aria-describedby="dlgDesc">
<h2 id="dlgTitle">確認</h2>
<p id="dlgDesc">この操作は元に戻せません。</p>
<!-- ボタン群 -->
</dialog>
もう一つ、現場で覚えておきたいのが closedby 属性です。新しめの仕様で、closedby="any" を付けると背景クリックでも閉じる挙動が標準で入ります。対応ブラウザではJSの背景クリック処理が不要になるのですが、まだ対応が広がっている途中なので、当面は前述のJSパターンを併用しておくのが安全です。
まとめ
- <dialog> 要素は、フォーカス管理・Escキー対応・他要素の不活性化など、モーダルに必要な振る舞いをブラウザネイティブで提供してくれる。
- モーダルとして使う場合は必ず showModal() を呼ぶこと。show() は非モーダル用なのでフォーカストラップが効かない。
- <form method="dialog"> を使うと、JSなしで「閉じるボタン」が作れる上、ボタンの value を returnValue として受け取れる。
- 背景クリックで閉じる挙動は、e.target === dialog の判定を使った数行のJSで実装できる。
- キャンセル系のボタンには formnovalidate を付けて、必須項目のバリデーションを回避するのがポイント。
- 開閉アニメーションは @starting-style と transition の allow-discrete を使えばCSSだけで実装可能。
- 背景は ::backdrop 疑似要素でスタイリング、見出しと説明文は aria-labelledby / aria-describedby で関連付けると、よりアクセシブルになる。