削除ボタンを押したときに「本当に削除しますか?」と出す、あのJavaScriptのconfirmダイアログ。手軽で便利なんですが、見た目がブラウザ任せなのでサイトのデザインからめちゃくちゃ浮くんですよね。デザイナーさんから「ここだけ急にOSっぽいの何?」と突っ込まれた経験がある人も多いのではないでしょうか。今回はconfirmをカスタマイズしたい人向けに、バニラJSと<dialog>要素だけでデザイン自由な確認ダイアログを作る方法を、HTML・CSS・JavaScriptのサンプルコード付きでご紹介します。
そもそもJavaScriptのconfirm()はCSSでカスタマイズできない
結論から言ってしまうと、window.confirm()の見た目はCSSでは一切いじれません。ボタンの色を変えたい、角を少し落としたい、フォントをサイトに合わせたい……と思っても無理なんです。というのも、confirmのダイアログはページのDOM内にあるHTML要素ではなく、ブラウザが提供しているネイティブのUIだからです。ページ側のCSSセレクタでは掴みようがないんですよね。
MDNの説明を読むと、confirmはメッセージ付きのダイアログを表示して、ユーザーがOKかキャンセルを押すまで待つようブラウザに指示するメソッドとされています。戻り値はOKならtrue、キャンセルならfalseのbooleanです。つまり私たち開発者が渡せるのは「メッセージの文字列」だけ。ボタンの文言を「削除する」「やめておく」に変えることすらできません。
if (window.confirm('本当に削除しますか?')) {
deleteItem();
}この手軽さは正直ありがたいです。ただ、ChromeとSafariとFirefoxで見た目がバラバラで、しかもダークモード対応のサイトだと完全に浮きます。ECサイトの購入フローなどでこれが出ると、ユーザーに「なんか怪しい警告が出た」と思われかねないのも地味に痛いところです。
confirmにはデザイン以外の落とし穴もある
見た目だけの問題かというと、そうでもありません。MDNには、ユーザーがタブを切り替えたときなど条件によってはダイアログが表示されなかったり、ユーザーの回答を待たなかったりする場合がある、と明記されています。さらにブラウザがページ内ダイアログを無視している状態だと、戻り値は常にfalseになります。「キャンセルしてないのに処理が止まる」という謎挙動の原因がこれだったりするんですよね。
もう一つ、実務で遭遇しやすいのがiframe内での挙動です。iframeのsandbox属性にallow-modalsが付いていないとconfirmがブロックされます。CMSのプレビュー画面や埋め込みウィジェットの中で「confirmが出ない!」とハマったら、まずここを疑ってみてください。ちなみに今回紹介するdialog要素は、allow-modalsがなくても使えます。
confirmをカスタマイズする3つの方法と、今おすすめの選択肢
confirmそのものは変えられないので、「confirmっぽく動く自前のダイアログ」を作って置き換えるのが現実的な答えになります。作り方は大きく分けて3パターンあります。昔ながらのdivで自作する方法、HTML標準の<dialog>要素を使う方法、そしてSweetAlert2のようなライブラリに頼る方法です。それぞれ一長一短あるので、ざっくり比較してみましょう。
| 方法 | 手軽さ | デザイン自由度 | アクセシビリティ | 依存 |
|---|---|---|---|---|
| divで自作 | △ | ◎ | ×(全部自前) | なし |
<dialog>要素 | ○ | ◎ | ○(ブラウザ任せでOK) | なし |
| ライブラリ | ◎ | ○ | ○ | あり |
divで自作する方法は「やること多すぎ問題」
数年前まではこれが定番でした。オーバーレイ用のdivをposition: fixedで全面に敷いて、その上にモーダル本体を乗せるやつです。見た目は完全に自由ですが、ESCキーで閉じる処理、背景をクリックできなくする処理、フォーカスをモーダル内に閉じ込める処理、aria-modalの付与……と、地味な実装がどんどん積み上がっていきます。ぶっちゃけ、ここを全部ちゃんとやっている現場はかなり少ないと思います。
ライブラリは便利だけど、確認ダイアログ1個のためには重い
SweetAlert2などのライブラリを入れれば、見た目もアニメーションも一瞬で整います。ただ、確認ダイアログを1〜2箇所出したいだけのために外部ライブラリを読み込むのは、ちょっと大げさな気もするんですよね。デザインもライブラリ側のクセが残るので、サイト独自のトンマナに完全に合わせようとすると、結局CSSの上書き合戦になりがちです。
結論:今は<dialog>要素が第一候補
そこで今回の本命が<dialog>要素です。showModal()で開けばモーダルとして表示され、ESCキーで閉じる挙動も、背景を操作不可にする挙動も、aria-modal="true"相当の扱いもブラウザが勝手にやってくれます。しかも中身はただのHTMLなので、CSSで好きなだけデザインできます。MDNのconfirmのページでも、確認用途には<dialog>要素を使う選択肢が紹介されているので、公式のお墨付きと言ってもいいでしょう。とはいえ「一択」というわけではなく、既存のUIフレームワークのモーダルで統一している現場や、複雑なアニメーション・独自のフォーカス制御が必要なケースでは、ライブラリや独自実装を選ぶのも十分アリです。
【コピペOK】confirmをカスタマイズしたサンプルコード(HTML・CSS・JavaScript)
前置きが長くなったので、さっそくコードを見ていきましょう。今回は「記事を削除する」ボタンを押すと、デザインされた確認ダイアログが出てくるサンプルです。ライブラリは一切使わず、バニラJSだけで完結しています。ダークモードにも自動で追従するようにしているので、そのままコピペして動かしてみてください。
HTML
ポイントは<dialog>の中に<form method="dialog">を置いているところです。こうすることで、ボタンを押しただけでダイアログが閉じて、押したボタンのvalueがreturnValueに入ります。JavaScriptでボタンごとにクリックイベントを書かなくていいので、かなりスッキリしますね。
<button type="button" id="deleteBtn" class="btn btn-primary">記事を削除する</button>
<dialog id="confirmDialog" class="confirm-dialog"
aria-labelledby="confirmTitle" aria-describedby="confirmMessage">
<form method="dialog" class="confirm-dialog__inner">
<h2 id="confirmTitle" class="confirm-dialog__title">確認</h2>
<p id="confirmMessage" class="confirm-dialog__message"></p>
<div class="confirm-dialog__actions">
<button value="cancel" class="btn btn-ghost" autofocus>キャンセル</button>
<button value="ok" class="btn btn-primary">OK</button>
</div>
</form>
</dialog>CSS
見た目はミニマルに、ボーダーではなく余白と影で浮かせています。背景の暗幕は::backdrop疑似要素で指定できるので、オーバーレイ用のdivはもう要りません。色はCSS変数にまとめて、prefers-color-schemeでダークモード時だけ差し替えています。
:root {
--accent: #d90368;
--bg: #ffffff;
--text: #1f1f1f;
--muted: #6b6b6b;
--surface: #f2f2f2;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1c1c1e;
--text: #f2f2f2;
--muted: #a0a0a0;
--surface: #2c2c2e;
}
}
.confirm-dialog {
width: min(90vw, 400px);
padding: 0;
border: none;
border-radius: 8px;
background: var(--bg);
color: var(--text);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
}
.confirm-dialog::backdrop {
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(2px);
}
.confirm-dialog[open] {
animation: dialog-in 0.2s ease-out;
}
@keyframes dialog-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: none; }
}
.confirm-dialog__inner { padding: 32px 28px 24px; }
.confirm-dialog__title { margin: 0 0 12px; font-size: 1.125rem; }
.confirm-dialog__message {
margin: 0 0 28px;
color: var(--muted);
line-height: 1.7;
white-space: pre-wrap; /* \n で改行させる */
}
.confirm-dialog__actions { display: flex; justify-content: flex-end; gap: 12px; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 0.9375rem;
cursor: pointer;
}
.btn-ghost { background: var(--surface); color: var(--text); }
.btn-primary { background: var(--accent); color: #fff; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* 補助:表示中は背面ページのスクロールも止める(任意) */
body:has(dialog[open]) { overflow: hidden; }JavaScript
customConfirm()という関数を作って、Promiseで結果を返すようにしています。本家のconfirmと同じく「OKならtrue、それ以外ならfalse」が返ってくるので、awaitを付ければほぼ同じ感覚で書けます。ボタンの文言やタイトルもオプションで変えられるようにしました。
const dialog = document.getElementById('confirmDialog');
const titleEl = document.getElementById('confirmTitle');
const messageEl = document.getElementById('confirmMessage');
const okBtn = dialog.querySelector('button[value="ok"]');
const cancelBtn = dialog.querySelector('button[value="cancel"]');
function customConfirm(message, {
title = '確認',
okText = 'OK',
cancelText = 'キャンセル',
} = {}) {
titleEl.textContent = title;
messageEl.textContent = message;
okBtn.textContent = okText;
cancelBtn.textContent = cancelText;
dialog.returnValue = ''; // 前回の値をリセット
dialog.showModal();
return new Promise((resolve) => {
dialog.addEventListener('close', () => {
resolve(dialog.returnValue === 'ok');
}, { once: true });
});
}
// 背景(::backdrop)クリックでキャンセル扱いにする
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close('cancel');
});
// 使い方
document.getElementById('deleteBtn').addEventListener('click', async () => {
const ok = await customConfirm('この記事を削除します。\nこの操作は取り消せません。', {
title: '記事を削除しますか?',
okText: '削除する',
cancelText: 'やめておく',
});
if (ok) {
console.log('削除処理を実行');
} else {
console.log('キャンセルされました');
}
});サンプルコードのポイント解説
コピペで動けばそれでOK、という方はここを読み飛ばしても大丈夫です。ただ、あとで「ESCで閉じたときに変な値が返ってくる」「2回目以降の挙動がおかしい」といった細かいバグに悩まされないためにも、各パーツが何をしているのかは押さえておくと安心です。特に大事な4つのポイントを順番に見ていきましょう。
show()ではなくshowModal()を使う
<dialog>を開くメソッドにはshow()とshowModal()の2種類があります。confirmの代わりとして使うなら、必ずshowModal()の方を使ってください。showModal()で開いたダイアログは最前面のトップレイヤーに表示され、背面のページは操作できなくなります。ESCキーで閉じる挙動もこちらだけの機能です。
show()で開くと、ただページ上に要素が現れるだけの「非モーダル」な状態になります。背面のボタンも普通に押せてしまうので、確認ダイアログとしては意味がありません。MDNによると、showModal()で開いた場合は暗黙的にaria-modal="true"として扱われるので、スクリーンリーダーへの配慮という面でもこちら一択ですね。
form method=”dialog”とreturnValueでどのボタンが押されたか判定する
<dialog>内のformをmethod="dialog"で送信すると、ダイアログが閉じて、クリックされたボタンのvalueがreturnValueにセットされます。データがサーバーに送信されることはありません。今回のサンプルでは、OKボタンにvalue="ok"、キャンセルボタンにvalue="cancel"を付けて、閉じたあとにreturnValueを見て判定しています。
ここで地味に大事なのが、開くたびにdialog.returnValue = ''でリセットしている点です。ESCキーで閉じた場合はreturnValueが更新されないため、リセットしないと前回押したボタンの値が残ってしまいます。前回OKを押していると、ESCで閉じたのに削除が実行される……という恐ろしい事故が起きるので、ここは絶対に省略しないでください。
Promiseでラップしてawaitで書けるようにする
本家のconfirmは処理を同期的に止めてくれますが、自作ダイアログはそうはいきません。ダイアログを開いた瞬間に後続の処理が走ってしまいます。そこでcloseイベントを待つPromiseを返すようにして、await customConfirm(...)と書けるようにしています。これでif (confirm(...))とほぼ同じ書き味になります。
addEventListenerの第3引数に{ once: true }を付けているのもポイントです。これを付けないと、ダイアログを開くたびにcloseイベントのリスナーが積み重なっていきます。何度か開閉したあとに、昔のPromiseがまとめて解決されて処理が多重実行される、というなかなか気付きにくいバグの原因になります。
::backdropで背景の暗幕をデザインする
showModal()で開いたダイアログの背面には、::backdropという疑似要素が自動で敷かれます。今回は半透明の黒にbackdrop-filter: blur(2px)を足して、背景をうっすらぼかしてみました。昔はこのためにオーバーレイ用のdivを用意してz-indexと格闘していたので、だいぶ楽になりましたね。
ちなみに背景クリックで閉じる処理は、ダイアログ本体のpaddingを0にして、クリックされた要素がdialog自身かどうかで判定しています。中身のformが全面を覆っているので、e.target === dialogになるのは::backdrop部分をクリックしたときだけ、という仕組みです。Chromeなどではclosedby="any"属性でも同じことができますが、Safariでは2026年現在、通常版ではまだ対応しておらず、Technology Previewなどで対応が進んでいる段階です。古いSafariやiOS端末も考えると、現時点ではJavaScriptによる実装をフォールバックとして用意しておくのが無難でしょう。
カスタムconfirmを実務に入れるときの注意点
サンプルをそのまま使えば大体の場面はカバーできますが、実際の案件に組み込もうとすると「あれ?」となるポイントがいくつかあります。ここでは、confirmを自作ダイアログに置き換えるときに特につまずきやすいところをまとめておきます。全部覚える必要はないので、ハマったときに思い出してもらえればOKです。
フォーム送信前の確認はpreventDefaultしてから送り直す
一番多いのがこれです。本家のconfirmならonsubmit="return confirm('送信しますか?')"の一行で済んでいたものが、自作ダイアログだとそのままでは動きません。Promiseの結果を待っている間に、フォームがさっさと送信されてしまうからです。一度e.preventDefault()で送信を止めて、OKだったらform.submit()やrequestSubmit()で改めて送信する、という流れにしましょう。
form.addEventListener('submit', async (e) => {
if (form.dataset.confirmed) return; // 確認済みの再送信だけ素通り
e.preventDefault();
if (await customConfirm('この内容で送信しますか?')) {
form.dataset.confirmed = 'true';
form.requestSubmit(); // submitイベントは同期的に発火する
delete form.dataset.confirmed; // フラグはすぐ解除
}
});ここでは、確認後にプログラムから再送信するときに、もう一度確認処理に入らないよう一時的なフラグを使っています。フラグを立てっぱなしにすると、バリデーションエラーで差し戻されたあとの再送信で確認が出なくなるので、requestSubmit()の直後に解除しているのがポイントです。実際のフォームでは、Ajax送信やサーバー側のエラー処理も含めて、フラグの解除タイミングを設計しておいてください。
また、確認ダイアログ自体にも<form>を使っているので、送信したいフォームの内側にダイアログを置かないように注意してください。formの入れ子はHTMLとして不正なので、ブラウザによっては内側のformが無視されて予期しない送信が走ります。ダイアログはbody直下など、フォームの外に置いておくのが安全です。
危険な操作では「キャンセル」に初期フォーカスを当てる
サンプルでは、キャンセルボタンにautofocus属性を付けています。showModal()で開いたとき、ダイアログ内のautofocus付き要素にフォーカスが移るので、初期フォーカスがキャンセル側になり、うっかりEnterキーを押してしまっても誤って削除されにくくなります。削除や決済のような取り返しのつかない操作ほど、安全側のボタンを初期選択にしておくのがUI的にも親切です。
逆に「保存しますか?」のように、OKが基本ルートになる確認ならOKボタンにautofocusを移しても問題ありません。このあたりは本家のconfirmでは制御できなかった部分なので、自作ならではのメリットと言えますね。フォーカスリングも:focus-visibleでアクセントカラーにしておくと、キーボード操作のユーザーにも分かりやすくなります。
confirmを完全に置き換えられない場面もある
最後に一つだけ。自作ダイアログは非同期なので、処理を同期的に止める必要がある場面ではconfirmの代わりになりません。代表例がページ離脱時のbeforeunloadです。ここはブラウザ標準の確認しか出せない仕様なので、デザインを揃えたくても諦めるしかありません。
とはいえ、削除・送信・ログアウトといった普段の確認ダイアログは、多くの場面で今回の方法に置き換えられます。ECサイトのカート削除やマイページの退会ボタンなど、ユーザーの目に触れやすい場所から順番に差し替えていくと、サイト全体の統一感がグッと上がりますよ。
まとめ
いかがでしたでしょうか?JavaScriptのconfirmはブラウザのネイティブUIなので、CSSでカスタマイズすることはできません。デザインを揃えたいなら、confirmっぽく動く確認ダイアログを自作して置き換えるのが正解です。そして今なら、HTML標準の<dialog>要素を使うのが手軽で有力な方法になります。
押さえておきたいポイントをおさらいすると、まず開くときはshow()ではなくshowModal()を使うこと。ボタンの判定は<form method="dialog">とreturnValueに任せて、開くたびにreturnValueをリセットすること。そしてPromiseでラップしてawaitで書けるようにすること。この3つさえ守れば、本家confirmとほぼ同じ感覚で、見た目だけ自由にデザインできる確認ダイアログが手に入ります。
divで全部自作していた頃は、ESCキー対応やフォーカス管理、背景のスクロール制御まで自前で書く必要があって、正直かなり面倒でした。それがブラウザ標準の機能だけでここまでスッキリ書けるようになったのは、フロントエンドエンジニアとしては素直にうれしいところです。外部ライブラリに頼らなくていいので、読み込み速度やメンテナンスの面でも安心ですね。
今回のサンプルは、ダークモードにも自動で対応しているので、そのままコピペしてプロジェクトに組み込んでも大丈夫です。色や余白をサイトのデザインに合わせて調整すれば、「ここだけOSっぽい」問題とはおさらばできます。テンプレートリテラルを使ってメッセージを組み立てる方法は『JavaScriptの文字列をスッキリ複数行で書く方法【テンプレートリテラル】』、ダイアログの挙動確認には『Google Chromeデベロッパーツールのショートカットおススメ10選』の記事もあわせてどうぞ。