ジャンルから探す

Web Design English

dialog要素の改善解説と使い方【HTML】

モーダルを作るなら、自作のdivではなく<dialog>要素を使うのが2026年時点の正解です。フォーカスの閉じ込め、背景の操作無効化、Escキーでのキャンセル、::backdropによる背景の装飾を、ブラウザが標準で面倒を見てくれます。

<dialog>は2022年3月に全モダンブラウザで使えるようになり、現在は Baseline(widely available)です。さらにその後、閉じ方を制御するclosedby属性と、@starting-styleによるアニメーションが加わって、実務で足りない部分がほぼ埋まりました。

この記事では、show()showModal()の違いという基本から、実際に詰まるポイント(背景クリックで閉じる、閉じるアニメーション、戻り値の受け取り方)まで解説します。

スポンサーリンク

show() と showModal() は何が違うのか

結論として、ほとんどの場面で使うべきなのはshowModal()です。show()はブラウザが何も守ってくれません。

項目 showModal() show()
背景の操作 不可(自動でinert相当になる) 可能
フォーカスの閉じ込め あり なし
Escキーで閉じる 閉じる(cancelイベント) 閉じない
::backdrop 効く 効かない
ARIA aria-modal="true" aria-modal="false"
重なり順 トップレイヤー(z-indexの影響を受けない) 通常のフロー

show()が向くのは、ページの操作を続けながら参照したいパネル(検索の絞り込み、チャットウィンドウなど)だけです。この場合はEscで閉じる処理を自分で書く必要があります。

非モーダルダイアログの実装

See the Pen
dialog01
by Rin (@rinblog0408)
on CodePen.

<button id="open-dialog">非モーダルダイアログを開く</button>

<dialog id="non-modal-dialog">
  <p>非モーダルです。背景のページも操作できます。</p>
  <button id="close-dialog" autofocus>閉じる</button>
</dialog>

<script>
  const dialog = document.getElementById('non-modal-dialog');
  const openButton = document.getElementById('open-dialog');
  const closeButton = document.getElementById('close-dialog');

  openButton.addEventListener('click', () => {
    dialog.show(); // 非モーダルで表示
  });

  closeButton.addEventListener('click', () => {
    dialog.close();
  });

  // 非モーダルは Esc で閉じないので自分で実装する
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && dialog.open) dialog.close();
  });
</script>

最後のEscキー処理が非モーダルでは必須です。これが無いと、キーボードだけで操作しているユーザーが閉じられません。

スポンサーリンク

モーダルダイアログの実装

See the Pen
dialog02
by Rin (@rinblog0408)
on CodePen.

<button id="open-modal-dialog">モーダルダイアログを開く</button>

<dialog id="modal-dialog">
  <h2>確認</h2>
  <p>モーダルです。閉じるまで背景は操作できません。</p>
  <button id="close-modal-dialog" autofocus>閉じる</button>
</dialog>

<script>
  const modalDialog = document.getElementById('modal-dialog');

  document.getElementById('open-modal-dialog')
    .addEventListener('click', () => modalDialog.showModal());

  document.getElementById('close-modal-dialog')
    .addEventListener('click', () => modalDialog.close());
</script>

autofocusを付けている点に注目してください。付けないとダイアログ内の最初のフォーカス可能要素に自動で当たります。「削除する」のような破壊的なボタンが先頭にある場合は、必ず安全な側(キャンセル)にautofocusを置いてください。Enterキーの連打で誤操作が起きます。

なお、<dialog>自体にtabindexを付けてはいけません。ダイアログの枠は操作対象ではないためです。

背景クリックで閉じるには(closedby属性)

「ダイアログの外側をクリックしたら閉じる」は最も要望の多い挙動ですが、既定では閉じません。これを1行で実現するのが closedby 属性です。

<dialog id="dlg" closedby="any">
  <p>外側クリックでもEscでも閉じます</p>
  <button autofocus>閉じる</button>
</dialog>
閉じ方
none ボタンやclose()だけ。Escでも閉じない
closerequest Esc+開発者が用意した手段(showModal()の既定)
any 外側クリック(light dismiss)+Esc+開発者の手段

2026年9月時点で、closedbyは Chrome / Edge / Firefox が対応済み、Safari が未対応です(Interop 2026 の対象項目)。したがって、当面はフォールバックを併記するのが安全です。

const dlg = document.getElementById('dlg');

// closedby 非対応ブラウザ向けのフォールバック
if (!('closedBy' in dlg)) {
  dlg.addEventListener('click', (e) => {
    // ダイアログ自身(=背景部分)がクリック対象ならば外側クリック
    if (e.target === dlg) dlg.close();
  });
}

この判定が成り立つのは、<dialog>のボックスの外側(backdrop領域)のクリックが<dialog>自身のイベントとして届くためです。中身を<div>で1枚包んでおくと、内側クリックが誤って閉じる事故を防げます。

逆に、入力途中の内容を守りたいフォームでは closedby="none" にして、明示的な「キャンセル」ボタン以外では閉じないようにする使い方もできます。

スポンサーリンク

フォームの結果を受け取る(method=”dialog”)

<dialog>の中のフォームにmethod="dialog"を指定すると、送信せずにダイアログを閉じ、押されたボタンのvaluereturnValueに入ります。確認ダイアログの実装がとても短くなります。

<dialog id="confirm-dialog">
  <form method="dialog">
    <p>この投稿を削除しますか?</p>
    <button value="cancel" autofocus>キャンセル</button>
    <button value="delete">削除する</button>
  </form>
</dialog>
const confirmDialog = document.getElementById('confirm-dialog');

confirmDialog.addEventListener('close', () => {
  if (confirmDialog.returnValue === 'delete') {
    deletePost();
  }
});

confirmDialog.showModal();

イベントは2種類あります。使い分けを間違えると閉じたのに処理が走らない、という不具合になります。

  • close:閉じたとき必ず発火する。returnValueを読むのはこちら
  • cancel:Escで閉じられたときに発火する。returnValueは更新されない

Escで閉じた場合、returnValueは空文字のままです。上のコードのように「特定の値のときだけ実行する」書き方にしておけば、Escキャンセルも自然に安全側へ倒れます。

開閉をアニメーションさせる

<dialog>display: nonedisplay: blockを行き来するため、素直にtransitionを書いても効きません。@starting-styletransition-behavior: allow-discreteを使います。

dialog {
  opacity: 0;
  transform: translateY(-16px);
  transition:
    opacity .3s ease-out,
    transform .3s ease-out,
    overlay .3s ease-out allow-discrete,
    display .3s ease-out allow-discrete;
}

/* 開いている状態 */
dialog:open {
  opacity: 1;
  transform: translateY(0);
}

/* 開く瞬間の開始値 */
@starting-style {
  dialog:open {
    opacity: 0;
    transform: translateY(-16px);
  }
}

/* 背景も一緒にフェードさせる */
dialog::backdrop {
  background: rgb(0 0 0 / 0);
  transition: background .3s ease-out, overlay .3s ease-out allow-discrete, display .3s ease-out allow-discrete;
}
dialog:open::backdrop {
  background: rgb(0 0 0 / .5);
}
@starting-style {
  dialog:open::backdrop { background: rgb(0 0 0 / 0); }
}

@media (prefers-reduced-motion: reduce) {
  dialog, dialog::backdrop { transition: none; }
}

ポイントは3つです。

  • displayoverlaytransitionに含め、allow-discreteを付ける:これが無いと閉じるアニメーションが再生されずに即消えます
  • @starting-styleで開始値を書く:開くときのアニメーションはこれが無いと効きません
  • prefers-reduced-motionを必ず用意する

スクロール・背景まわりでよくある問題

モーダルを開いている間、背景のページがスクロールしてしまうことがあります。showModal()は背景の操作を止めますが、スクロールは止めません

/* モーダル表示中は背景のスクロールを止める */
body:has(dialog[open]) {
  overflow: hidden;
}

また、ダイアログ内が長くなる場合は、ダイアログ自身に高さの上限とスクロールを与えます。

dialog {
  max-width: min(90vw, 640px);
  max-height: 85dvh;
  overflow: auto;
  border: none;
  border-radius: 12px;
  padding: 24px;
}

高さの単位にdvhを使うと、スマートフォンのアドレスバーの伸縮で下部が切れる問題を避けられます。

まとめ

  • モーダルはshowModal()show()はフォーカス閉じ込めもEscも::backdropも無い
  • autofocus安全な側のボタンに置く。<dialog>本体にtabindexは付けない
  • 外側クリックで閉じるならclosedby="any"Safari未対応なのでe.target === dialogのフォールバックを併記
  • 結果の受け取りは<form method="dialog">returnValueclosecancelの違いに注意
  • アニメーションは@starting-styletransition-behavior: allow-discretedisplayoverlayを含める)
  • 背景のスクロールはbody:has(dialog[open]) { overflow: hidden; }で止める

自作モーダルで一番作り込みが必要なのはフォーカス管理とEsc対応ですが、<dialog>はそこを丸ごと引き受けてくれます。CSSの:has()セレクターと組み合わせると、開閉状態に応じたスタイル制御もJavaScript無しで書けます。