HTMLとCSSで、JavaScriptなしでダイアログを開閉する方法
ボタンに commandfor="ダイアログのid" と command="show-modal" を書くと、クリックで <dialog> がモーダルで開きます。閉じるボタンには command="close" を書きます。showModal() を呼ぶ JavaScript は要らず、Esc キーで閉じる動きや、閉じたあとにフォーカスが元のボタンへ戻る動きも、ブラウザーが受け持ちます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML のページをヘッドレスの Chromium で開き、Playwright でクリックとキー操作をして、ダイアログの状態とイベントを記録しました。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げは試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

開くボタンと閉じるボタンに commandfor と command を書く
HTML は次のとおりです。開くボタンも閉じるボタンも、commandfor にダイアログの id を書き、command に何をするかを書きます。
<button id="btn" commandfor="dlg" command="show-modal">設定を開く</button>
<dialog id="dlg">
<p>通知をメールで受け取りますか?</p>
<button commandfor="dlg" command="close" value="no">受け取らない</button>
<button commandfor="dlg" command="close" value="yes">受け取る</button>
</dialog>
MDN の button 要素の説明によると、show-modal は showModal() を、close は close() を呼ぶのと同じ働きです。
検証環境で「設定を開く」をクリックすると、ダイアログはモーダルで開き、:modal に当てはまりました。フォーカスはダイアログの中の最初のボタン「受け取らない」へ移りました。後ろの「設定を開く」の位置にある要素を document.elementFromPoint() で取ると、ボタンではなく DIALOG が返りました。「受け取る」をクリックするとダイアログが閉じ、フォーカスは「設定を開く」へ戻りました。
キーボードでも、Enter キーや Space キーで開き、Esc キーで閉じました。
押されたボタンは returnValue で分かる
閉じるボタンに value を書くと、その値がダイアログの returnValue に入ります。上の HTML で「受け取る」を押して閉じると returnValue は "yes"、「受け取らない」なら "no" でした。値を読むところだけは JavaScript が要ります。ダイアログの close イベントで returnValue を見ると、どちらで閉じたかが分かります。
検証環境の Chromium 153 では、returnValue は次のように変わりました。
| 操作 | returnValue |
|---|---|
「受け取る」(value="yes")で閉じる |
"yes" |
| そのあと、もう一度開いた直後 | "yes" のまま |
| 開いたダイアログを Esc キーで閉じる | "" |
value のない close ボタンで閉じる |
前の値("yes")のまま |
MDN の returnValue の説明には、Esc キーなどで閉じたときは値が設定されないとありますが、Chromium 153 では "" に戻りました。また、value のないボタンで閉じても、前に入った値が残ります。returnValue を読むなら、ダイアログを閉じるボタンにはすべて value を書いておきます。
背景のクリックで閉じるには closedby="any" を足す
何も指定しないと、ダイアログの外(後ろの暗くなった部分)をクリックしても閉じません。<dialog> に closedby 属性を書くと、閉じ方を変えられます。
<dialog id="dlg" closedby="any">
検証環境で、ダイアログを開いてから画面の右下の隅をクリックし、まだ開いていれば Esc キーを押しました。
| closedby | ダイアログの外をクリック | Esc キー |
|---|---|---|
| 指定なし | 閉じない | 閉じる |
closerequest |
閉じない | 閉じる |
any |
閉じる | ―(外のクリックで閉じたため試していない) |
none |
閉じない | 閉じない |
MDN では、showModal() で開いたダイアログに closedby がないときは closerequest と同じ扱いになると説明されています。command="show-modal" で開いたときも、表のとおり同じ動きでした。MDN によると、none では閉じるボタンなど、ページの側で用意した方法でしか閉じられません。
form の中に置くときは type="button" を書く
検証環境の Chromium 153 では、開くボタンを <form> の中に置くと、type を書かないままでは、クリックしてもダイアログが開きませんでした。フォームの送信も起きませんでした。
| ボタンの置き場所と type | クリックした結果 |
|---|---|
form の外、type なし |
開いた |
form の中、type なし |
開かない(送信もしない) |
form の中、type="button" |
開いた |
form の中、type="submit" |
開かずにフォームを送信した |
type を書かないボタンの type プロパティは、form の中でも "button" を返しました。プロパティを読んでも、開かない理由は分かりません。フォームの中では type="button" を書いておきます。
CSS で、開いている間の見た目を変える
開いたダイアログの後ろには ::backdrop が出ます。検証環境では、何も指定しないときの背景色は rgba(0, 0, 0, 0.1) でした。冒頭の GIF では、次の CSS で後ろを灰色にしています。
dialog::backdrop {
background: rgb(31 42 48 / 0.35);
}
#btn:has(+ dialog:open) {
background: #eefafd;
}
:open は、開いている <dialog> や <details> に当てはまる擬似クラスです。2つ目の指定は、これと :has() を組み合わせて、ダイアログが開いている間だけ、開くボタン(id="btn")の背景色を変えます。検証環境で、開くボタンの直後に <dialog> を置き、ボタンの背景色を getComputedStyle() で読むと、開いている間だけ指定どおりの色になり、閉じると元の色に戻りました。GIF では、ボタンが ::backdrop の下にあるため、この色の違いは見分けにくくなっています。:open は、Safari では commandfor より後の 26.5 から使えます(最後の表を参照)。
request-close なら、閉じる前に止められる
command="request-close" のボタンを押すと、ダイアログはすぐには閉じません。先に cancel イベントが起き、そこで preventDefault() を呼ぶと閉じるのを止められます(MDN の button 要素の説明)。止める条件を書くところは JavaScript になります。
const dlg = document.getElementById("dlg");
const memo = document.getElementById("memo");
dlg.addEventListener("cancel", (e) => {
if (memo.value) e.preventDefault();
});
検証環境で、ダイアログの中に入力欄 memo と、request-close のボタン、close のボタンを置きました。入力欄が空のときは、request-close のボタンで閉じました。文字を入れたあとは、request-close のボタンでも Esc キーでも閉じませんでした。close のボタンは、cancel イベントを起こさずにそのまま閉じました。
command に書ける値と、書き間違えたときの動き
MDN の button 要素の説明によると、ダイアログに使える値は show-modal、close、request-close の3つです。ほかに、ポップオーバー用の show-popover、hide-popover、toggle-popover と、-- で始まる独自の値があります。MDN では、commandfor は popovertarget 属性をより汎用的にしたものと説明されています。
値を書き間違えても、エラーは出ず、ダイアログが開かないだけでした。検証環境で試した結果は次のとおりです。
command="open"、command="showModal"、commandなしのボタンは、押しても開かず、ダイアログにcommandイベントも起きないcommandforにページにないidを書いたときや、<dialog>にshow-popoverのように対象の要素に合わない値を書いたときも開かない(show-popoverではcommandイベントだけは起きる)command="SHOW-MODAL"のように大文字で書いても、show-modalとして開く
開かないときは、ボタンの command プロパティを読むと見当がつきます。open のような使えない値を書いたボタンでは "" が返りました。
JavaScript から、押されたことを知る
commandfor で指定された要素には、ボタンが押されるたびに command イベントが起きます(MDN の CommandEvent の説明)。イベントの command には押されたボタンの command の値が、source には押されたボタンが入ります。
dlg.addEventListener("command", (e) => {
console.log(e.command, e.source.id);
});
検証環境では、show-modal のボタンを押すと、ダイアログが開く前にこのイベントが起き、ここで preventDefault() を呼ぶと開きませんでした。--reset のような -- で始まる値では、イベントが起きるだけで、ダイアログは開きませんでした。
対応しているブラウザー
互換性データでは、各ブラウザーが次のバージョンから対応しています。
| ブラウザー | commandfor、command | request-close | closedby | :open |
|---|---|---|---|---|
| Chrome、Edge | 135 | 139 | 134 | 133 |
| Android の Chrome | 135 | 139 | 134 | 133 |
| Firefox | 144 | 144 | 141 | 136 |
| Safari | 26.2 | 26.2 | Technology Preview | 26.5 |
| iOS の Safari | 26.2 | 26.2 | 未対応 | 26.5 |
closedby は、対応していないブラウザーがまだあります。外のクリックで閉じられなくても困らないように、ダイアログの中には閉じるボタンを必ず置いておきます。
対応しているかどうかは、HTMLButtonElement に command プロパティがあるかで調べられます。検証環境では true が返りました。
const supported = "command" in HTMLButtonElement.prototype;





