HTMLとCSSで、JavaScriptなしでダイアログを開閉する方法

​​CSS​HTML,Playwright

ボタンに 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)の値です。

ボタンとダイアログの画面のアニメーション。上に「操作 1回目/4回:「設定を開く」をクリック」のような表示と、ダイアログの状態、returnValue の値が出ている。矢印のカーソルで「設定を開く」を押すと、ピンクの丸が出て、ダイアログ「通知をメールで受け取りますか?」が開き、後ろが灰色になる。「受け取る」を押すとダイアログが閉じ、returnValue が "yes" とピンクで表示される。もう一度開いて Esc キーを押すと、「Esc」のキーの表示が出てダイアログが閉じ、returnValue は "" になる
「設定を開く」で開き、「受け取る」と Esc キーで閉じるまで。上の表示は、操作や値が変わった瞬間だけピンクになる

開くボタンと閉じるボタンに commandfor と command を書く

HTML は次のとおりです。開くボタンも閉じるボタンも、commandfor にダイアログの id を書き、command に何をするかを書きます。

html
<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 属性を書くと、閉じ方を変えられます。

html
<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 で後ろを灰色にしています。

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 になります。

js
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 には押されたボタンが入ります。

js
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 が返りました。

js
const supported = "command" in HTMLButtonElement.prototype;

​Posted by 86