🖱️ ボタンにcommand属性を足すだけで、JSのモーダル制御コードを消せた話

Chrome135・Firefox144・Safari26.2で対応が出揃ったHTML Invoker Commands APIで、ダイアログとポップオーバーのJS制御を宣言的に置き換えた実体験と落とし穴をまとめた。

モーダルの開閉くらい、JSで数行書けばいいと思っていた。でも、showModal()close()をボタンごとにaddEventListenerで束ねているうちに、気づけば1つのポップオーバーコンポーネントに50行近いJSがぶら下がっていた。イベント委譲、aria-expandedの同期、Escapeキーの二重処理。触るたびに壊れる箇所が増えていく感覚があった。

commandcommandforで、その50行が消えた

Chrome 135・Firefox 144・Safari 26.2 で揃って対応が完了した、HTMLのInvoker Commands APIというやつを試してみた。ボタンにcommandfor(対象要素のid)とcommand(実行するアクション)を書くだけで、ダイアログやポップオーバーの開閉を宣言的に制御できる。

<button commandfor="mymodal" command="show-modal">開く</button>
<dialog id="mymodal">
  <button commandfor="mymodal" command="close">閉じる</button>
</dialog>

これだけで、モーダルの開閉ロジックがHTML側に収まった。JSのダウンロードを待たずに動くので、初期表示のもたつきも減る。地味だけど、効いてくる変化だと思う。

落とし穴: 既存のpopovertargetと混在させると事故る

一点、やらかしかけた。popovertarget属性で書いていた古いポップオーバーと、新しいcommandforを同じページに混在させたら、片方のボタンだけ反応しなくなった。commandfor/commandpopovertarget/popovertargetactionの後継として設計されていて、置き換えるなら一気に書き換えたほうがいい。中途半端な移行は事故のもとになる。

カスタムコマンドも書ける

組み込みのshow-popovercloseだけでなく、--から始まる名前で独自コマンドを定義できる。commandイベントを拾って自分の処理を書けば、タブ切り替えやアコーディオンのような複雑なUIも、JSを最小限に抑えたまま作れる。

<button commandfor="tabs" command="--select-tab-2">タブ2</button>
tabs.addEventListener('command', (e) => {
  if (e.command === '--select-tab-2') { /* 切り替え処理 */ }
});

「ハンドラをHTML属性に書くのは悪習」という指摘が昔からあるのは知っている。ただ、onclickのような文字列評価ではなく、宣言的な属性とイベントリスナーの組み合わせなので、性質はだいぶ違う。

しばらくは新規のモーダル・ポップオーバーからcommand/commandforに寄せていくつもりだ。既存コードを一気に書き換える勇気はまだ出ないけど。

元ネタ: https://www.infoq.com/news/2026/01/html-invoker-commands/