[popover vs command] dialog要素を制御するベストプラクティス

今年、button要素 に command属性 と commandfor属性 が追加され、すべてのモダンブラウザで使用可能になりました。これにより dialog要素 を表示するための JavaScript が不要になります。ということで今回は、dialog要素 を制御するためのベストプラクティスを紹介します。

以前、dialog要素 を使用したドロワーメニューの記事を書きました。この記事では、ドロワーメニューをポップオーバーで表示しました。モーダルで表示するには、JavaScript の HTMLDialogElement.showModal() による実装が必要で、HTML と CSS だけで完結できなかったためですが、command属性 と commandfor属性 によってモーダルも JavaScriptなし で実装できるようになりました。

/* ポップオーバーダイアログ */
<p><button popovertarget="popover-dialog" popovertargetaction="show">dialog open</button></p>
<dialog id="popover-dialog" popover="auto">
  …
  <p><button popovertarget="popover-dialog" popovertargetaction="hide">dialog close</button></p>
</dialog>

/* JavaScript を簡易的に使用したモーダルダイアログ */
<p><button aria-controls="js-modal-dialog" onclick="document.getElementById('js-modal-dialog').showModal();">dialog open</button></p>
<dialog id="js-modal-dialog">
  …
  <p><button aria-controls="js-modal-dialog" onclick="document.getElementById('js-modal-dialog').close();">dialog close</button></p>
</dialog>

/* JavaScript を使用しないモーダルダイアログ */
<p><button commandfor="modal-dialog" command="show-modal">dialog open</button></p>
<dialog id="modal-dialog">
  …
  <p><button commandfor="modal-dialog" command="close">dialog close</button></p>
</dialog>

ポップオーバーとモーダルの違い

ポップオーバーは、ダイアログ外のタブ移動やスクロールが可能で、ダイアログ外をクリックすると閉じることができます。対してモーダルは、ダイアログ外の操作を無効化し、閉じるボタンを押すといった特定の操作を行わない限り閉じることはありません。

また、それらとは別に、ポップオーバーとモーダルの特徴を併せ持った非モーダルと呼ばれるものもあります。これは、閉じるボタンを押さない限り表示させておくといった使い方なので、JavaScript による制御が必須のようです。

ポップオーバーモーダル非モーダル
目的補足情報の表示, 一時的なメニューユーザーへの確認, 重要な入力, 操作の強制補足情報の表示, 一時的なメニュー
ダイアログ外の操作可能不可能可能
フォーカスの制御ダイアログ内外でフォーカスできる。ダイアログ内のみフォーカスできる。ダイアログ内外でフォーカスできる。
閉じ方ダイアログ外のクリックを含む様々な方法。閉じるボタンや Escキー などの所定の方法。閉じるボタンなどの所定の方法。
実装方法popover属性 を設定した任意の要素と、popovertarget属性 と popovertargetaction属性、もしくは command属性 (show-popover, hide-popover, toggle-popover) と commandfor属性 を設定したボタン (input要素, button要素)。dialog要素 と、command属性 (show-modal, close) と commandfor属性 を設定した button要素。dialog要素 を HTMLDialogElement.close() と HTMLDialogElement.show() で制御。
アクセシビリティ非破壊的。読み上げ順序に注意が必要。強制的。スクリーンリーダーが内容を強調。非破壊的。読み上げ順序に注意が必要。
使用例ツールチップ, ドロップダウンメニューログイン画面, 確認画面, 全画面ドロワーメニューお知らせ, チャット
ポップオーバーとモーダルの比較

ユーザーの操作を邪魔せずにダイアログを表示したいのであればポップオーバーや非モーダル、ユーザーの操作を中断させ特定の操作を求めるのであればモーダルを使用するという使い分け方になります。

CodePen

See the Pen A dialog element whose behavior changes depending on the button by nov (@numerofive) on CodePen.

dialog要素 に popover属性 を設定するのは適切か?

上記 CodePen の HTMLコード には、dialog要素 にポップオーバーとして表示するボタンと、モーダルとして表示するボタンが設定されており、どちらも問題なく機能します。しかし、command属性 と commandfor属性 が dialog要素 を操作することに特化した属性であるのに対し、ポップオーバーとして表示させる popover属性 は任意の要素に設定できるグローバル属性なので、文脈的に相応しい要素があれば、その要素にポップオーバーを設定すべきだと思います。

<!-- 前後の内容に関連する記事の紹介など -->
<p><button popovertarget="popover-article" popovertargetaction="show">関連する記事を見る</button></p>
<article id="popover-article" popover="auto">
  …
  <p><button popovertarget="popover-article" popovertargetaction="hide">閉じる</button></p>
</article>

<!-- 前後の内容に関する補足情報など -->
<p><button popovertarget="popover-aside" popovertargetaction="show">補足情報を見る</button></p>
<aside id="popover-aside" popover="auto">
  …
  <p><button popovertarget="popover-aside" popovertargetaction="hide">閉じる</button></p>
</aside>

<!-- 目次や関連ページなどのリンク一覧 -->
<p><button popovertarget="popover-nav" popovertargetaction="show">目次を見る</button></p>
<nav id="popover-nav" popover="auto">
  …
  <p><button popovertarget="popover-nav" popovertargetaction="hide">閉じる</button></p>
</nav>

<!-- サイト内検索や絞り込み検索など -->
<p><button popovertarget="popover-search" popovertargetaction="show">検索する</button></p>
<search id="popover-search" popover="auto">
  <form … > … </form>
  <p><button popovertarget="popover-search" popovertargetaction="hide">閉じる</button></p>
</search>

ただし、これらの要素は dialog要素 と異なり、表示したときのスタイルがデフォルトでは設定されていないので、独自に設定する必要があります。

[popover]:is(article, aside, nav, search) {
  background: Canvas;
  color: CanvasText;
  inset: 0;
  margin: auto;
  max-block-size: 100%;
  max-inline-size: 100%;
  overflow: auto;
  overscroll-behavior: contain;
  padding: 2rem;
  position: fixed; /* 通常は自動的に付与されるが念のため */

  &:popover-open {
    /* 表示したときに追加する設定 */
  }
}

popovertarget属性 と popovertargetaction属性 を使用すべきか?

ボタンに設定する popovertarget属性 と popovertargetaction属性 はポップオーバーに特化した属性であるのに対し、command属性 と commandfor属性 はポップオーバーとモーダルで使用できるため、後者に一元化した方が良いでしょう。

popovertargetaction属性値command属性値
ポップオーバーを表示showshow-popover
ポップオーバーを非表示hidehide-popover
ポップオーバーの表示切替toggletoggle-popover
モーダルを表示show-modal
モーダルを非表示close
popovertargetaction属性 と command属性 の比較
/* 古いポップオーバー */
<p><button popovertarget="old-popover" popovertargetaction="show">dialog open</button></p>
<dialog id="old-popover" popover="auto">
  …
  <p><button popovertarget="old-popover" popovertargetaction="hide">dialog close</button></p>
</dialog>

/* 新しいポップオーバー */
<p><button commandfor="new-popover" command="show-popover">dialog open</button></p>
<dialog id="new-popover" popover="auto">
  …
  <p><button commandfor="new-popover" command="hide-popover">dialog close</button></p>
</dialog>

CSS によるスタイリング方法の違い

dialog要素 に開閉アニメーションを設定するなど、表示されている時にだけスタイルを設定したい場合がありますが、これが中々に複雑です。

JavaScript の HTMLDialogElement.showModal() または HTMLDialogElement.show() で表示された場合は、dialog要素 に open属性 が付与され、属性セレクタ ([open]) が使用できます。

JavaScript の HTMLDialogElement.showModal() または button[command="show-modal"] で表示された場合は、:modal擬似クラス が使用できます。

JavaScript の HTMLElement.showPopover() または button[command="show-popover"] (button[popovertargetaction="show"]) で表示された場合は、:popover-open擬似クラス が使用できます。

[open]:modal:popover-open
ポップオーバー使用不可能使用不可能使用可能
モーダル使用可能使用可能使用不可能
非モーダル使用可能使用不可能使用不可能
表示方法による使用可能なセレクタの違い
dialog:popover-open {
  /* ポップオーバーで表示されたダイアログ */
}
dialog[open] {
  /* HTMLDialogElementインターフェイス で表示されたダイアログ */
}
dialog:modal {
  /* モーダルで表示されたダイアログ */
}
dialog[open]:modal {
  /* HTMLDialogElement.showModal() で表示されたダイアログ */
}
dialog[open]:not(:modal) {
  /* HTMLDialogElement.show() で表示されたダイアログ */
}

dialog要素 は、ダイアログAPI (Dialog API), ポップオーバーAPI (Popover API), 呼び出しコマンドAPI (Invoker Commands API) という異なる仕様が絡み合って中々にカオスですが、正しく使いこなしましょう。

関連記事