【JavaScript応用講座】OK/キャンセルを美しく!カスタムダイアログの作り方

はじめに

本日、indexedDBを活用して、アセット管理するシステムがほぼ完成。

ZIPデータをindexedDBへBlobデータとして保存した後、indexedDBからアセットを選択してデータを丸ごと入れかえと、 アセット内のデータを閲覧できる実装をしたほか、新たな試みとして、タップ操作でカードを自由に配置閲覧できる仕組みも構築。

ナビゲーションドロワーも実装しましたが、まだメニューが機能するまで実装出来てないのでそれは明日以降の課題になると思います。

今回は、以下の動画の中でも出てますが、ゲーム制作をしていると何かと必要なりがちな、確認ダイアログ。

JavaScriptには標準で、window.confirm()という大変便利なダイアログボックスを作る仕組みがあるのですが、 見た目が兎に角味気ないので、乱用的に使えて、やや多機能な、カスタムダイアログを本日開発の途中で実装したので、その紹介をしてみます。

1. window.confirm()のデメリット

🤔 なぜ confirm() を卒業するべきか? JavaScriptでおなじみの window.confirm() は手軽で便利ですが、以下のようなデメリットがあります:

🧱 デメリット一覧

問題点 説明
ブロッキング動作 confirmはスレッドを止める同期処理です。他のUI操作や非同期処理と相性が悪く、ユーザー体験が悪くなります。
🎨 スタイルを変更できない OSやブラウザ依存のデザインで、カスタマイズが一切できない。見た目の統一感を崩す要因に。
📱 モバイルで使いにくい モバイル環境ではポップアップの挙動が不安定で、UXが不安定になることが多いです。
🔄 再利用性がない confirm() はプレーンテキストのみ。タイトルやアイコンの変更、内容に応じた表示ができない

✅ 結論:

confirm() は “最低限の安全確認” には使えるが、 洗練されたUI/UXやゲーム的演出を求める場合は不向き。

2. HTMLでカスタムダイアログの構造を作る

💡 選択肢は大きく分けて2つ:

方法 特徴 向いている用途
<dialog> タグ HTML5で追加された専用要素。簡単・組み込みで制御可能 シンプルな確認やフォーム表示
DIV構成 + CSS より柔軟なカスタムUIが作れる。IEでもOK 高度な演出・スタイル・多段モーダルなど

今回はより汎用性が高い「DIV構成」でいきます。

以下に基本構造のHTMLを示します。

<div id="custom-dialog" class="dialog hidden">
  <div class="dialog-backdrop"></div>
  <div class="dialog-box">
    <div class="dialog-header">
      <span class="dialog-title">確認</span>
    </div>
    <div class="dialog-body">
      <p>この操作を実行してもよろしいですか?</p>
    </div>
    <div class="dialog-footer">
      <button class="dialog-btn cancel">キャンセル</button>
      <button class="dialog-btn ok">OK</button>
    </div>
  </div>
</div>

📌 各要素の役割

  • #custom-dialog:全体を覆うモーダル用ラッパー(非表示時は .hidden)
  • .dialog-backdrop:背景を薄暗くする要素(クリック遮断用)
  • .dialog-box:中央に表示される本体
  • .dialog-header:タイトル表示
  • .dialog-body:本文(メッセージ表示)
  • .dialog-footer:ボタン(OK / キャンセル)

🔁 表示・非表示の切り替えは、CSSとJSで制御します 次項で CSSデザインと表示アニメーション を行い、さらにその次で非同期Promise制御を実装します。

✅ 3. CSSでデザイン

モーダル表示・フェード演出・ボタンスタイルの整備

🎨 目的

  • ユーザーの視線を中央に集めるレイアウト
  • 薄暗い背景(バックドロップ)で操作不可感を出す
  • OK/キャンセルボタンに意味を持たせた色分け
  • 出現・消失時にフェードアニメーション

💄 CSS全体例(基本編)

/* 非表示用 */
.hidden {
  display: none;
}

/* 全体を覆うモーダル */
.dialog {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1000;
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 背景を薄暗く */
.dialog-backdrop {
  position: absolute;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.4);
}

/* ダイアログ本体 */
.dialog-box {
  position: relative;
  background: #fff;
  border-radius: 10px;
  padding: 1.5em;
  width: 90%;
  max-width: 400px;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.25);
  animation: fadeIn 0.2s ease;
  z-index: 1;
}

/* ヘッダー・本文・フッター */
.dialog-header {
  font-weight: bold;
  font-size: 1.2em;
  margin-bottom: 0.5em;
}
.dialog-body {
  margin-bottom: 1em;
}
.dialog-footer {
  text-align: right;
}

/* ボタン */
.dialog-btn {
  padding: 0.5em 1em;
  margin-left: 0.5em;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
}
.dialog-btn.ok {
  background-color: #4caf50;
  color: white;
}
.dialog-btn.cancel {
  background-color: #f44336;
  color: white;
}

/* フェードアニメ */
@keyframes fadeIn {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}

📱 補足:モバイルでの表示調整

@media (max-width: 480px) {
  .dialog-box {
    width: 95%;
    font-size: 0.9em;
  }
}

✅ ここまでで実現できること:

  • 中央固定のダイアログUI
  • 薄暗い背景でモーダル感を演出
  • アニメーションで自然な出現
  • OKとキャンセルの視認性UP

次は一番重要なポイント:

「JavaScriptで非同期にPromise対応させる」

つまり、await showDialog(“本当に削除しますか?”) みたいに書けるようにします。

4. JavaScriptで「OK / キャンセル」の非同期制御(Promiseでラップ)

🎯 目的

  • window.confirm() と同じように使えるようにする
  • でも UI は自作CSS+HTMLのカスタム版
  • await showDialog(“〇〇しますか?”) の形で使えると理想的!

🧩 実装ステップ

① HTMLから要素を取得

const dialog = document.getElementById("custom-dialog");
const titleEl = dialog.querySelector(".dialog-title");
const bodyEl = dialog.querySelector(".dialog-body");
const okBtn = dialog.querySelector(".dialog-btn.ok");
const cancelBtn = dialog.querySelector(".dialog-btn.cancel");

② showDialog関数をPromiseで定義

function showDialog(message, options = {}) {
  return new Promise((resolve) => {
    // タイトル・本文のセット
    titleEl.textContent = options.title || "確認";
    bodyEl.innerHTML = `<p>${message}</p>`;

    // 表示
    dialog.classList.remove("hidden");

    // OKクリックでtrue
    okBtn.onclick = () => {
      dialog.classList.add("hidden");
      resolve(true);
    };

    // キャンセルクリックでfalse
    cancelBtn.onclick = () => {
      dialog.classList.add("hidden");
      resolve(false);
    };
  });
}

③ 使用例

async function handleDelete() {
 const confirmed = await showDialog("このカードを削除してもよろしいですか?");
 if (confirmed) {
   console.log("削除実行");
   // 実際の削除処理など
 } else {
   console.log("キャンセルされました");
 }
}

📦 オプション拡張(例)

await showDialog("すべてのデータを消去します。続行しますか?", {
  title: "⚠️ 警告"
});

✅ ポイントまとめ

内容 説明
非同期で柔軟なUI制御が可能 複雑な処理フローに対応
UIは自由にデザイン可 confirm()ではできない、色・位置・内容変更が自在
モバイルにも適応しやすい モーダルの見た目やボタン配置も調整可能

次は実例編です:「カード削除」や「ストレージリセット前の警告」で実際にこのカスタムダイアログを使ってみます。

5. 実例編:カスタムダイアログの活用ケース

🃏 ① カード削除の確認ダイアログ

想定シーン:

ユーザーが画面上のカードをクリック or ゴミ箱アイコンを押して、削除するか確認したい場合。

async function deleteCard(cardId) {
  const confirmed = await showDialog("このカードを削除してもよろしいですか?", {
    title: "カード削除の確認"
  });

  if (confirmed) {
    // 実際の削除処理
    removeCardFromView(cardId);
    removeCardFromDB(cardId);
    console.log("カード削除完了:", cardId);
  } else {
    console.log("キャンセルされました");
  }
}

📝 ユーザー視点での流れ:

  1. ゴミ箱アイコンを押す
  2. 「このカードを削除しますか?」という 中央に出るモーダルが表示
  3. OK → 削除処理実行
  4. キャンセル → 何もしない

💾 ② ストレージリセット前の警告

想定シーン:

アセットや設定をすべて消去して「初期状態に戻す」操作をユーザーが行おうとした場合。

async function resetStorage() {
  const confirmed = await showDialog(
    "すべてのアセットと設定が初期化されます。本当に実行してもよろしいですか?",
    { title: "⚠️ ストレージリセットの警告" }
  );

  if (confirmed) {
    await clearIndexedDB();  // IndexedDB全削除
    await loadDefaultAsset(); // 初期データ読み込み
    console.log("ストレージリセット完了");
  } else {
    console.log("リセット処理はキャンセルされました");
  }
}

⚠️ ポイント

  • タイトルに「⚠️」を入れることで、警告的な意味合いが強調される
  • 実行前にユーザーに心理的確認を促すことができる(事故防止)

💡 応用可能なユースケース例

  • ログアウト確認
  • 設定の初期化
  • ZIPアセットの削除
  • セーブデータの上書き
  • ギャラリーモードの一時停止

続いてラストは、 「info / warn / error」などの種類別にスタイル切り替えする拡張解説です。

6. ダイアログに「info / warn / error」などの種類別スタイルを適用する拡張

🎯 目的

  • 単なる「確認」だけでなく、状況(情報/警告/エラー)に応じた視覚的表現を追加する
  • 視覚でユーザーに判断の重要度を伝える
  • showDialog() のオプションで簡単に切り替えられると理想的

🧩 追加CSS(種別クラスを定義)

.dialog-box.info {
  border-left: 6px solid #2196f3; /* 青:情報 */
}
.dialog-box.warn {
  border-left: 6px solid #ff9800; /* オレンジ:警告 */
}
.dialog-box.error {
  border-left: 6px solid #f44336; /* 赤:エラー */
}

必要に応じて背景やアイコンも加えてOKです。

例:.dialog-header::before { content: “⚠️”; } など

🔧 JavaScriptでクラスを切り替える(showDialog内に追記)

function showDialog(message, options = {}) {
  return new Promise((resolve) => {
    const type = options.type || "info"; // デフォルトはinfo

    // タイトル・本文
    titleEl.textContent = options.title || "確認";
    bodyEl.innerHTML = `<p>${message}</p>`;

    // 一旦すべての種別クラスを削除 → 必要なものを追加
    dialog.querySelector(".dialog-box").classList.remove("info", "warn", "error");
    dialog.querySelector(".dialog-box").classList.add(type);

    // 表示
    dialog.classList.remove("hidden");

    // OK
    okBtn.onclick = () => {
      dialog.classList.add("hidden");
      resolve(true);
    };

    // キャンセル
    cancelBtn.onclick = () => {
      dialog.classList.add("hidden");
      resolve(false);
    };
  });
}

✅ 使用例(拡張版)

ℹ️ 情報メッセージ

await showDialog("アップデートが完了しました。", {
  title: "お知らせ",
  type: "info"
});

⚠️ 警告メッセージ

await showDialog("この操作は元に戻せません。実行してよろしいですか?", {
  title: "⚠️ 警告",
  type: "warn"
});

❌ エラーメッセージ


await showDialog("ファイルの読み込みに失敗しました。", {
  title: "エラー",
  type: "error"
});

🔚 総まとめ:この拡張で何が変わる?

機能 メリット
type指定による装飾切替 視認性とUX向上。直感的に理解できる
見た目で意図が伝わる ボタンより先に「これは重大だ」と伝えられる
再利用性の向上 アプリ全体の警告UIを共通化・統一できる