画面の上に小さな窓を出して、後ろを暗くする。この「モーダルウィンドウ」は、今のHTMLなら <dialog> という専用のタグで作れます。自分で作ると手間のかかるEscキーでの閉じ方やキーボードの動きを、ブラウザが受け持ってくれます。
使いどころは、今の作業を止めてでも答えてほしいときです。削除の確認が代表です。読むだけのお知らせや、ページを開いた直後の案内には使いません。読んでいる人の手を止めるので、止める理由があるときだけにします。
この記事の要点
<dialog>とshowModal()で、後ろを操作できなくする・Escで閉じる・いちばん手前に出す、がブラウザの標準の動きになる- 使いどころは、削除の確認のように、今の作業を止めてでも答えてほしいとき。読むだけのお知らせには使わない
- 後ろのページのスクロールは止まらないので自分で止める。やり直せない操作では、最初に選ばれるボタンを「キャンセル」にする
完成見本:モーダルの4つの形
見本は、小さな画面の中で「ボタンを押す→開く→閉じる」を自動でくり返しています。
※ 自動で動く見本は、本物を開くと記事が読めなくなるので、画面の中に同じCSSで再現しています。読み上げソフトでは読まれないようにしています。本物を開いて試せる見本は、記事の後半にあります。
コピペ用のコード
CSSは、サイトのCSSファイルや、WordPressならテーマの「追加CSS」などに入れます。JavaScriptは、ページの最後(</body> の直前)で読み込みます。クラス名の頭の kd-dlg は、サイトのほかのデザインと名前がぶつからないようにするための目印です。
1のCSSとJavaScriptは、2〜4でも使います。1ページに複数置くときは、id と aria-labelledby の名前が重ならないように変えてください。
1. 基本のモーダル
<dialog> の中に中身を書き、JSの showModal() で開きます。閉じるボタンは <form method="dialog"> の中に置くだけで、JSを書かなくても閉じます。暗い背景は ::backdrop(モーダルの後ろに敷かれる層)で色を付けます。
<button type="button" class="kd-dlg-btn" data-dialog="kd-notice">お知らせを見る</button>
<dialog class="kd-dlg" id="kd-notice" aria-labelledby="kd-notice-title">
<form method="dialog" class="kd-dlg-in">
<p class="kd-dlg-title" id="kd-notice-title">臨時休業のお知らせ</p>
<p class="kd-dlg-text">12月29日から1月3日までお休みします。</p>
<div class="kd-dlg-actions">
<button class="kd-dlg-btn" autofocus>閉じる</button>
</div>
</form>
</dialog>/* 1〜4で使う:モーダルの見た目 */
.kd-dlg {
box-sizing: border-box;
width: min(420px, calc(100vw - 32px));
padding: 0;
border: 2px solid transparent; /* 強制カラーモードで枠が見えるように */
border-radius: 14px;
background-color: #FFFFFF;
box-shadow: 0 18px 44px rgba(16, 40, 58, 0.28);
color: #10283A;
}
.kd-dlg::backdrop {
background-color: rgba(16, 40, 58, 0.55);
}
.kd-dlg-in {
padding: 24px;
}
.kd-dlg-title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 700;
line-height: 1.4;
}
.kd-dlg-text {
margin: 0 0 20px;
font-size: 15px;
line-height: 1.7;
color: #33495B;
}
.kd-dlg-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin: 0;
}
.kd-dlg-btn {
min-height: 44px;
padding: 0 18px;
border: 2px solid #0369A1;
border-radius: 8px;
background-color: #0369A1;
color: #FFFFFF;
font-size: 15px;
font-weight: 700;
cursor: pointer;
}
.kd-dlg-btn:focus-visible {
outline: 3px solid #0284C7;
outline-offset: 2px;
}
/* 開いている間、後ろのページをスクロールさせない */
html:has(.kd-dlg:modal) {
overflow: hidden;
scrollbar-gutter: stable;
}// data-dialog に書いたIDのモーダルを開く(1〜4で共通)
document.querySelectorAll('[data-dialog]').forEach(function (btn) {
btn.addEventListener('click', function () {
document.getElementById(btn.dataset.dialog).showModal();
});
});2. ふわっと出す
:modal は「showModal() で開いているモーダル」を指す疑似クラスです。ここに短いアニメーションを付けると、開くたびに動きます。背景も一緒に透明から濃くします。
<button type="button" class="kd-dlg-btn" data-dialog="kd-fade">ふわっと開く</button>
<dialog class="kd-dlg kd-dlg-fade" id="kd-fade" aria-labelledby="kd-fade-title">
<form method="dialog" class="kd-dlg-in">
<p class="kd-dlg-title" id="kd-fade-title">メールを送信しました</p>
<p class="kd-dlg-text">内容をご確認のうえ、お待ちください。</p>
<div class="kd-dlg-actions">
<button class="kd-dlg-btn" autofocus>閉じる</button>
</div>
</form>
</dialog>.kd-dlg-fade:modal {
animation: kd-dlg-in 0.25s ease-out;
}
.kd-dlg-fade::backdrop {
animation: kd-dlg-bg 0.25s ease-out;
}
@keyframes kd-dlg-in {
from {
opacity: 0;
transform: translateY(12px) scale(0.98);
}
}
@keyframes kd-dlg-bg {
from {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.kd-dlg-fade:modal,
.kd-dlg-fade::backdrop {
animation: none;
}
}3. 下から出す(外側を押して閉じる)
margin で画面の下にくっつけ、幅を100%にします。外側を押して閉じる動きは短いJSで足します。中身を .kd-dlg-in で包み、モーダル本体の余白を0にしてあるのが仕掛けです。押された場所がモーダル本体そのものなら「外側」と判断できます。
<button type="button" class="kd-dlg-btn" data-dialog="kd-sheet">下から開く</button>
<dialog class="kd-dlg kd-dlg-sheet" id="kd-sheet" aria-labelledby="kd-sheet-title">
<form method="dialog" class="kd-dlg-in">
<p class="kd-dlg-title" id="kd-sheet-title">並べかえ</p>
<p class="kd-dlg-text">新しい順に並んでいます。</p>
<div class="kd-dlg-actions">
<button class="kd-dlg-btn" autofocus>閉じる</button>
</div>
</form>
</dialog>.kd-dlg-sheet {
width: 100%;
max-width: 100%;
margin: auto 0 0;
border-radius: 16px 16px 0 0;
}
.kd-dlg-sheet .kd-dlg-in {
padding-bottom: calc(24px + env(safe-area-inset-bottom));
}
.kd-dlg-sheet:modal {
animation: kd-dlg-up 0.3s ease-out;
}
@keyframes kd-dlg-up {
from {
transform: translateY(100%);
}
}
@media (prefers-reduced-motion: reduce) {
.kd-dlg-sheet:modal {
animation: none;
}
}// 外側(暗いところ)を押したら閉じる
document.querySelectorAll('.kd-dlg-sheet').forEach(function (d) {
d.addEventListener('click', function (e) {
if (e.target === d) d.close();
});
});4. 削除の確認
autofocus を「キャンセル」に付けて、最初に選ばれているボタンを安全な側にします。value を付けたボタンで閉じると、その値が returnValue に入るので、どちらを押したかをJSで受け取れます。
<button type="button" class="kd-dlg-btn" data-dialog="kd-confirm">削除する</button>
<dialog class="kd-dlg" id="kd-confirm" aria-labelledby="kd-confirm-title">
<form method="dialog" class="kd-dlg-in">
<p class="kd-dlg-title" id="kd-confirm-title">この写真を削除しますか?</p>
<p class="kd-dlg-text">削除すると元に戻せません。</p>
<div class="kd-dlg-actions">
<button class="kd-dlg-btn kd-dlg-sub" value="cancel" autofocus>キャンセル</button>
<button class="kd-dlg-btn kd-dlg-danger" value="delete">削除する</button>
</div>
</form>
</dialog>.kd-dlg-sub {
background-color: #FFFFFF;
color: #0369A1;
}
.kd-dlg-danger {
border-color: #B91C1C;
background-color: #B91C1C;
}// どのボタンで閉じたかを受け取る
var kdConfirm = document.getElementById('kd-confirm');
kdConfirm.addEventListener('close', function () {
if (kdConfirm.returnValue === 'delete') {
/* ここに「削除する」を押したときの処理を書く */
}
kdConfirm.returnValue = ''; /* 次に開いたときのために空へ戻す */
});仕組み:ブラウザが受け持ってくれること
showModal() で何が起きるか
showModal() で開くと、ブラウザが次のことをしてくれます。MDNで確認できる内容と、Chromeで今回のコードを動かして確かめた結果です。
- いちばん手前に出る。「top layer」という専用の層に出るので、
z-indexの数字で競争しなくて済みます - 後ろが操作できなくなる。モーダルの外のボタンやリンクは押せません。Tabキーを押しても、後ろのページのボタンには移りませんでした
- Escキーで閉じる。JSを書かなくても閉じます
- 開いたらモーダルの中へ、閉じたら元のボタンへ。キーボードの操作位置(フォーカス)が移動し、閉じるともとのボタンに戻りました
<div> で自作すると、これらを全部JSで書くことになります。書き漏らすと、キーボードだけで操作する人が窓の外に迷い出たり、閉じられなくなったりします。
show() と showModal() は別物
似た名前の show() は、後ろも操作できる「モーダルではない」窓を開きます。Escでも閉じず、::backdrop も出ません。後ろを暗くしたいときは showModal() を使います。HTMLに open 属性を直接書く方法も、MDNでは勧められていません。
method=”dialog” のフォーム
<form method="dialog"> の中のボタンを押すと、データをどこにも送らず、モーダルだけが閉じます。ボタンに value があれば、その値が returnValue に入ります。4のJSでは、毎回最後に空へ戻して、前回の答えを次に持ち越さないようにしています。MDNの例でも、開くたびに空へ戻しています。
外側を押して閉じる、の新しい書き方
<dialog closedby="any"> と書くと、JSなしで外側を押して閉じられる新しい指定があります。MDNの対応表(8.1.8)では、Chrome 134・Firefox 141 から使えますが、Safariは正式版がまだ未対応です。今回は3のように短いJSで書きました。
数値を変えるとどうなるか
背景の暗さ
::backdrop の rgba() の最後の数字(濃さ)だけを変えました。
うすすぎると、後ろの文字が目に入って窓に集中できません。濃すぎると、別の画面に移ったように見えます。0.4〜0.6くらいが目安です。
出てくる速さ
2の animation の秒数だけを変えました。
1秒を超えると、押してから待たされている感じになります。0.2〜0.3秒くらいが目安です。
最初に選ばれているボタン
開いた直後に選ばれているボタンは、Enterキーを押すだけで実行されます。
「削除する」を最初にしていると、Enterキーのひと押しで消えます。W3Cの例でも、やり直せない操作では、いちばん安全なボタンを最初に選ぶとされています。
実際に試せる見本
ここのボタンは、コピペ用のコードそのままです。押すと本物のモーダルが開きます。Escキーや、Tabキーの動きも試してみてください。
スマホと、見る人への配慮
後ろのページのスクロールは止まらない
showModal() で後ろが操作できなくなっても、スクロールは別です。1のCSSの html:has(.kd-dlg:modal) は「開いているモーダルがあるときのページ全体」を指し、overflow: hidden で止めています。今回のコードでは、Chromeで開いている間にホイールを回してもページは動かず、閉じると動きました。scrollbar-gutter: stable は、スクロールバーが消えた分だけ中身が横にずれるのを防ぐ指定です。
中身が長いときと、スマホの幅
窓の幅は min(420px, calc(100vw - 32px)) にしています。広い画面では420px、スマホでは左右に16pxずつ残した幅です。中身が画面より長くなると、ブラウザの標準の指定によって窓の中がスクロールします。長い文章を読ませるなら、そもそもモーダルではなく別のページにします。
名前を付ける
aria-labelledby は、読み上げソフトに窓の名前(ここでは見出しの文)を伝える指定です。W3Cの例では、窓には名前が必要とされています。閉じるボタンも必ず置きます。MDNにも、閉じるボタンを明示するのがいちばん確実だと書かれています。
動きを減らす設定と、強制カラーモード
2と3は prefers-reduced-motion: reduce(動きを減らす設定)のときにアニメーションを切っています。枠の border: 2px solid transparent は、ふだんは見えない透明な枠です。Windowsのハイコントラスト表示のように影が消える環境で、窓のふちを残すために入れています。
やりすぎ注意
モーダルが続けて2回出る、開いた直後に全画面の広告が出る、閉じるボタンが小さくて押せない。どれも、読みに来た人の手を止めるだけになります。出す回数は、1回の操作で1つまでにします。
AIに頼むなら
Claudeなどの生成AIに頼むときは、タグと閉じ方まで指定します。指定しないと、<div> で自作したコードが出てくることがあります。
削除の確認モーダルを <dialog> と showModal() で作って。
閉じるのは <form method="dialog">、最初のフォーカスはキャンセルボタンに。
開いている間は後ろのページをスクロールさせないで。動きを減らす設定ではアニメーションなしで。AIが出したコードは、そのまま使う前に、次の3つを確かめます。
- キーボードだけで操作できるか。Tabキーで窓の外に出ないか、Escキーで閉じるか、閉じたあと元のボタンに戻るかを試します
z-indexや自作の暗い背景が残っていないか。showModal()を使うなら、どちらも要りません。<dialog>にtabindexを付けないこともMDNに書かれています- スマホの幅で、窓とボタンが収まるか。幅が固定になっていないか、ボタンが押しやすい大きさかを見ます
KAKERUならこう使う/使わない
最後に、作り手としての判断です。
使う場面
- 削除や取り消しの確認。4の形です。やり直せない操作の前で止めるのが、モーダルのいちばん正しい使い道です
- 入力の途中で、短い選択だけしてもらう場面。ページを移ると入力が途切れるときに使います
- スマホの絞り込みや並べかえ。3の形です。元の一覧がすぐ後ろに見えているので、迷いません
使わない場面
- ページを開いた直後のお知らせやメルマガの案内。読みに来た内容より先に、閉じる作業をお願いすることになります
- 長い説明や規約。窓の中で読ませず、ページに置きます
- 入力のエラー表示。どこを直すのかを見ながら直せるように、入力欄のすぐ近くに出します

モーダルは、読んでいる人の手を止める部品です。
KAKERUなら「止めてでも聞く理由があるか」を先に考えて、なければ出しません。
まとめ
<dialog>とshowModal()で、後ろの操作を止める・Escで閉じる・元のボタンへ戻す、がブラウザの標準の動きになる- 閉じるボタンは
<form method="dialog">に置く。どれを押したかはreturnValueで受け取る - 後ろのスクロールは止まらないので、
html:has(.kd-dlg:modal)で止める - やり直せない操作では、
autofocusを「キャンセル」に付ける - 使うのは、今の作業を止めてでも答えてほしいときだけ
まずは1のコードをコピーして、削除の確認など「止める理由」のある場所に入れてみてください。
参考にした資料
いずれも2026年11月9日に確認しました。ブラウザの対応状況は変わることがあります。最新の情報は各ページでご確認ください。
- MDN「<dialog>」(英語)
https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog
※showModal と show の違い、Escで閉じること、method=”dialog”、autofocus の考え方、tabindex を付けないこと。Baseline は Widely available(2022年3月から) - MDN「HTMLDialogElement: showModal()」(英語)
https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal
※いちばん手前の層(top layer)に出ること、ほかの部分が操作できなくなること - MDN「HTMLDialogElement: returnValue」(英語)
https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue
※ボタンの値が入ること、Escで閉じたときは設定されないこと - MDN「:modal」(英語)
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:modal
※showModal() で開いた dialog に当たる疑似クラス。Baseline は Widely available(2022年9月から) - MDN「::backdrop」(英語)
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::backdrop
※モーダルの後ろに敷かれる層 - MDN browser-compat-data 8.1.8
https://github.com/mdn/browser-compat-data
※closedby の対応状況(Chrome 134・Firefox 141。Safariは正式版が未対応) - W3C「Dialog (Modal) Pattern」(英語)
https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
※Tabの動き、Escで閉じること、閉じたら開いたボタンへ戻すこと、やり直せない操作では安全な側に最初のフォーカスを置くこと
ふだんの制作の話や、作ったものの裏側は Instagram と X でも出しています。





























連載「WEBの技術」の50回目です。
今回はモーダルです。作り方は簡単になりました。だからこそ「出すかどうか」の判断を中心に書きます。