はじめに:なぜ今さら dialog なのか

<dialog> 要素が登場してからもうすぐ10年になりますが、実務では依然として react-modal や sweetalert2 といったライブラリを導入するケースが少なくありません。理由はシンプルで、ネイティブ dialog の挙動がやや分かりにくいからです。show() と showModal() の違い、::backdrop のスタイリング、閉じた状態の display: none、アニメーションのトリガー条件など、情報が散在していて毎回調べ直すことになりがちです。

本記事ではその「調べ直し」を一度で終わらせるために、基本マークアップからアクセシブルな閉じるボタン、スクロールロック、登場・退場アニメーション、そして Popover API との選択基準までをコード付きで整理します。なお、本記事は CSS-Tricks の dialog スタイリング解説 を根拠資料として参照しています。

補足として、ダイアログを開いた際に背面のページは inert 状態になります。これはモーダルの場合のみで、show() で開いた場合はポップオーバーに近い挙動になる点は後述します。

Developer inspecting native HTML dialog element in browser DevTools with modal backdrop visible Algorithm Concept Visual

基本マークアップと開閉

最小構成は以下の通りです。

<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">
  <p>こんにちは、ダイアログです。</p>
  <button id="dialog-close">閉じる</button>
</dialog>

open 属性を直接付与すれば初期表示できますが、実務ではほぼ使われません。通常は JavaScript で開きます。

const dialogButton = document.querySelector('#dialog-button');
const dialog = document.querySelector('#dialog');

// show() はポップオーバー的な挙動(バックドロップ・中央配置・Esc 閉じなし)
dialogButton.addEventListener('click', () => {
  dialog.show();
});

// showModal() が真のモーダル(バックドロップ + 中央配置 + Esc 閉じ)
dialogButton.addEventListener('click', () => {
  dialog.showModal();
});

実務ではほぼ常に showModal() を選んでください。 show() はバックドロップもなく、中央配置もされず、Esc でも閉じません。ツールチップのような軽量なポップアップでない限り、showModal() が正解です。

閉じるボタン

close() メソッドで閉じます。closeModal() は存在しませんが、close() がモーダルも閉じます。

const dialogClose = document.querySelector('#dialog-close');
dialogClose.addEventListener('click', () => {
  dialog.close();
});

JavaScript なしでも可能です。form method="dialog" のテクニックです。

<form method="dialog">
  <button>閉じる</button>
</form>

アクセシブルな X ボタン

X アイコンだけを置くと、スクリーンリーダーが「エックス」と読み上げてしまいます。必ずテキストを視覚的に隠し、アイコンは aria-hidden を付与してください。

<button id="dialog-close">
  <span class="sr-only">ダイアログを閉じる</span>
  <svg aria-hidden="true" width="16" height="16">...</svg>
</button>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

初期フォーカスの制御

モーダルを開くと最初のフォーカスは閉じるボタンに当たります。その状態で Space キーを押すと意図せず閉じてしまうことがあります。フォームフィールドやリンクがある場合は、tabindex や autofocus で初期フォーカスを移すことを検討してください。

Web designer styling dialog and ::backdrop pseudo-element with CSS transitions in code editor Dev Environment Setup

スタイリング:::backdrop・位置・スクロールロック

バックドロップ

デフォルトのバックドロップは非常に薄く、ほとんど見えません。::backdrop 擬似要素で明示的にスタイリングします。

dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

ダイアログ本体

注意点として、dialog { ... } に直接スタイルを書いても効かないように見えます。閉じた状態のデフォルトが display: none のためです。[open] または :open 状態でスタイリングしてください。

dialog {
  border: 0;
  border-radius: 12px;
  padding: 24px;
  width: min(90vw, 480px);

  &[open] {
    background-color: #fff;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
  }
}

:modal 擬似クラスは :open より詳細度が高いため、上書きが必要な場面で有効です。ただし :open は比較的最近のサポートのため、レガシーブラウザを考慮するなら [open] 属性セレクタの方が安全です。

スクロールロック(背面スクロール防止)

モーダル表示中に背面がスクロールすると、ユーザーの文脈が失われます。最も広く互換性があるのは :has() を使う方法です。

body:has(dialog[open]) {
  overflow: hidden;
}

最新の Chrome(144+)では overscroll-behavior: contain をダイアログ自体に指定することで、より宣言的に処理できます。

dialog {
  overflow: hidden;
  overscroll-behavior: contain;

  &::backdrop {
    overscroll-behavior: contain;
  }
}

登場・退場アニメーション

transition だけでは動きません。display: none → block の切り替え時点で初期スタイルが存在せず、トランジションが発火しないためです。ここで @starting-style が必要になります。

@starting-style {
  dialog:open {
    opacity: 0;
    transform: translateY(-8px);
  }
}

dialog {
  transition: opacity 0.3s ease, transform 0.3s ease;

  &:open {
    opacity: 1;
    transform: translateY(0);
  }
}

View Transitions API はダイアログにはあまり向きません。モーダルは top layer に存在するため、close 時に old/new スナップショットのペアが安定して生成されないためです。登場のみ View Transition、退場は CSS アニメーションというハイブリッドも可能ですが、両方とも CSS transition/animation で統一した方が管理しやすいです。

Dialog vs Popover:どちらを使うべきか

Zell Liew の整理を実務目線で要約すると以下の通りです。

項目Dialog(showModal)Popover API
バックドロップ自動手動
フォーカストラップ自動手動(JS 必須)
背面の inert自動なし
Esc 閉じ自動自動
ARIA 連携自動手動で role 指定が必要
適した用途真のモーダルツールチップ、ドロップダウン、メニュー

まとめると、モーダルなら Dialog、それ以外のポップアップなら Popover です。Popover はアクセシビリティ機能(フォーカストラップ、inert、ARIA 連携)を自前で実装する必要があるため、真のモーダルが必要な場面で Popover を選ぶとアクセシビリティ負債が蓄積します。

Frontend developer comparing Dialog API and Popover API accessibility on laptop screen Developer Related Image

まとめ:今日から適用する3つのポイント

  1. モーダルは必ず showModal()。 show() はポップオーバー的な挙動で、バックドロップ・フォーカストラップ・背面 inert が付きません。
  2. スタイルは [open] または :modal 状態で指定。 閉じた状態の display: none のため、本体セレクタだけでは効きません。
  3. アニメーションには @starting-style が必須。 これがないと登場トランジションがスナップで終わってしまいます。

注意事項(批判的視点)

  • :open 擬似クラスは Safari 26.5 でようやくサポートされたレベルです。レガシー対応が必要なら [open] を使ってください。
  • overscroll-behavior: contain 方式は Chrome 144+ 依存のため、実務では body:has(dialog[open]) のフォールバックを併用するのが安全です。
  • Invoker Commands(command / commandfor 属性)はまだ実験段階です。本番投入は避け、ポリフィルやプログレッシブエンハンスメントの観点でのみ検討してください。
  • X アイコンに aria-hidden を付けないと、スクリーンリーダーが「エックス」と読み上げます。sr-only テキストとセットで使いましょう。

次のステップ学習の方向性

  • View Transitions API にまで発展させたい場合は、ダイアログではなくページレベルの遷移から感覚を掴むのがおすすめです。
  • Popover API を実務導入する際は、MDN の Popover アクセシビリティガイド を必ず併読してください。
  • ダイアログを用いたカスタムアニメーションパターン(例:shape() パスに沿うモーダル)は CSS-Tricks の関連記事 を参照してください。

あわせて読みたい記事

日本の開発エコシステムにおける適用文脈

日本の現場では依然として react-modal や sweetalert2 への依存が根強くありますが、<dialog> ネイティブへ移行することでバンドルサイズとアクセシビリティ負債を同時に削減できます。ただし、社内デザインシステムが独自アニメーションに依存している場合は、@starting-style の対応範囲(最新ブラウザ)を先に確認し、レガシー対象であれば JS ベースのアニメーションフォールバックを併用する戦略が安全です。Qiita でも dialog + @starting-style の組み合わせに関する投稿が増えてきており、モーダル実装のネイティブ回帰は今後さらに加速すると考えられます。

本コンテンツは、信頼性の高い情報源をもとにAIツールを活用して作成され、編集者によるレビューを経て公開されています。専門家によるアドバイスの代替となるものではありません。