確認ダイアログ(Confirm Dialog)— 削除確認パターン

通知・オーバーレイ 初級

このコンポーネントについて

確認ダイアログ(Confirm Dialog)は、削除・退会・初期化など取り消しのきかない操作の前にユーザーの意思を確認するために表示するポップアップです。 ブラウザ標準の window.confirm() の代わりとして、デザインをカスタマイズした実装が業務アプリで広く使われています。

このページでは実務でよく登場する3つのパターンを1つのデモ画面で体験できます。 プロフィールカードのアカウント削除・タスクの個別削除・全件削除を操作すると、確認後に画面上の要素が実際に変化する動作を確認できます。

  • Pattern 1 — 個別削除(ノーマル確認) — タスク行の「削除」ボタンで確認ダイアログを開き、確認するとその行がフェードアウトして消える
  • Pattern 2 — 全件削除(危険操作確認) — 赤いアクセントで危険操作であることを強調したダイアログ。すべてのタスクを一括削除する
  • Pattern 3 — アカウント削除(テキスト入力確認) — 「DELETE」と正確に入力するまで確定ボタンが押せない。GitHubのリポジトリ削除UIと同じパターン
  • 操作ログ — 確認・キャンセルの操作結果がタイムスタンプ付きで記録される

実装のポイント・注意点

ダイアログのHTML要素は1つだけ用意し、openDialog() 関数の引数でタイトル・メッセージ・スタイル(通常/危険)・コールバック関数を渡す設計にしています。 これにより、パターンが増えてもHTMLを追加せずにJavaScriptだけで対応できます。

Pattern 2(危険操作)の見た目の切り替えは dialog.className の直接代入で行います。 classList.add/remove ではなく className を上書きするのは、前回のクラスが残らないようにするためです。

Pattern 3(テキスト入力確認)では、inputoninput イベントで入力値と 'DELETE' を比較し、一致したときだけ確定ボタンの disabled を解除します。 Enterキーによる確定は無効にしています(テキスト入力中に誤ってEnterを押して確定してしまうのを防ぐため)。

タスク行の削除アニメーションは opacity: 0translateX を CSS transition で変化させ、300ms 後に li.remove() で DOM から削除します。 setTimeout の遅延時間は CSS の transition 時間と合わせてください。

テキストの挿入はすべて textContent を使っています。innerHTML に変数を渡すと XSS の原因になるため、動的なテキスト表示には必ず textContent を使います。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

山田 太郎

[email protected]

タスク一覧

    操作ログ
    • 操作するとここにログが表示されます

    サンプルソース

    3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
    ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>確認ダイアログ サンプル</title>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
    
    <!-- プロフィールカード(Pattern 3 の削除対象) -->
    <div class="cfm-profile" id="cfm-profile">
      <div class="cfm-profile-avatar" aria-hidden="true">👤</div>
      <div class="cfm-profile-info">
        <p class="cfm-profile-name">山田 太郎</p>
        <p class="cfm-profile-email">[email protected]</p>
      </div>
      <button class="cfm-account-del-btn" id="cfm-account-del-btn" type="button">
        アカウントを削除
      </button>
    </div>
    
    <!-- タスク一覧(Pattern 1: 個別削除 / Pattern 2: 全件削除) -->
    <div class="cfm-tasks">
      <div class="cfm-tasks-header">
        <h2 class="cfm-tasks-title">タスク一覧</h2>
        <button class="cfm-all-del-btn" id="cfm-all-del-btn" type="button">⚠ 全件削除</button>
      </div>
      <ul class="cfm-task-list" id="cfm-task-list"></ul>
      <p class="cfm-empty-msg" id="cfm-empty-msg" hidden>タスクがありません</p>
    </div>
    
    <!-- 操作ログ -->
    <div class="cfm-log">
      <div class="cfm-log-header">
        <span class="cfm-log-label">操作ログ</span>
        <button class="cfm-reset-btn" onclick="resetDemo()">リセット</button>
      </div>
      <ul class="cfm-log-list" id="cfm-log-list">
        <li class="cfm-log-empty">操作するとここにログが表示されます</li>
      </ul>
    </div>
    
    <!-- 確認ダイアログ(position: fixed で全画面に表示) -->
    <div class="cfm-overlay" id="cfm-overlay" hidden>
      <div class="cfm-dialog" id="cfm-dialog" role="dialog" aria-modal="true" aria-labelledby="cfm-dialog-title">
        <div class="cfm-dialog-header">
          <h2 class="cfm-dialog-title" id="cfm-dialog-title"></h2>
        </div>
        <div class="cfm-dialog-body">
          <p class="cfm-dialog-msg" id="cfm-dialog-msg"></p>
          <div class="cfm-input-wrap" id="cfm-input-wrap" hidden>
            <label class="cfm-input-label" for="cfm-input">「DELETE」と入力してください</label>
            <input class="cfm-input" id="cfm-input" type="text" placeholder="DELETE" autocomplete="off">
          </div>
        </div>
        <div class="cfm-dialog-footer">
          <button class="cfm-cancel-btn" id="cfm-cancel-btn" type="button">キャンセル</button>
          <button class="cfm-confirm-btn" id="cfm-confirm-btn" type="button"></button>
        </div>
      </div>
    </div>
    
    <script src="./script.js"></script>
    </body>
    </html>
    :root {
      --cfm-red: #EF4444;
      --cfm-red-dark: #DC2626;
      --cfm-red-light: #FEE2E2;
      --cfm-gray: #6B7280;
      --cfm-border: #E5E7EB;
      --cfm-radius: 8px;
    }
    
    *, *::before, *::after { box-sizing: border-box; }
    
    body {
      font-family: sans-serif;
      background: #F3F4F6;
      margin: 0;
      padding: 24px;
    }
    
    /* ================================================================
       プロフィールカード
    ================================================================ */
    .cfm-profile {
      background: #fff;
      border: 1.5px solid var(--cfm-border);
      border-radius: var(--cfm-radius);
      padding: 16px;
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 16px;
      max-width: 560px;
    }
    
    .cfm-profile-avatar {
      font-size: 28px;
      width: 48px;
      height: 48px;
      display: flex;
      align-items: center;
      justify-content: center;
      background: #F3F4F6;
      border-radius: 50%;
      flex-shrink: 0;
      transition: opacity 0.4s;
    }
    
    .cfm-profile-info { flex: 1; min-width: 0; }
    
    .cfm-profile-name {
      font-weight: 700;
      font-size: 15px;
      color: #111827;
      margin: 0 0 2px;
      transition: color 0.3s;
    }
    
    .cfm-profile-email {
      font-size: 13px;
      color: var(--cfm-gray);
      margin: 0;
      transition: color 0.3s;
    }
    
    .cfm-account-del-btn {
      padding: 7px 14px;
      font-size: 13px;
      font-weight: 500;
      color: var(--cfm-red);
      background: #fff;
      border: 1.5px solid var(--cfm-red);
      border-radius: 6px;
      cursor: pointer;
      font-family: sans-serif;
      white-space: nowrap;
      flex-shrink: 0;
      transition: background 0.15s;
    }
    .cfm-account-del-btn:hover:not(:disabled) { background: var(--cfm-red-light); }
    .cfm-account-del-btn:disabled { color: #9CA3AF; border-color: #D1D5DB; cursor: not-allowed; }
    
    /* 削除済みプロフィールの見た目 */
    .cfm-profile.is-deleted .cfm-profile-avatar { opacity: 0.3; }
    .cfm-profile.is-deleted .cfm-profile-name { color: #9CA3AF; text-decoration: line-through; }
    .cfm-profile.is-deleted .cfm-profile-email { color: #D1D5DB; }
    
    /* ================================================================
       タスク一覧
    ================================================================ */
    .cfm-tasks {
      background: #fff;
      border: 1.5px solid var(--cfm-border);
      border-radius: var(--cfm-radius);
      overflow: hidden;
      margin-bottom: 16px;
      max-width: 560px;
    }
    
    .cfm-tasks-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px 16px;
      border-bottom: 1px solid var(--cfm-border);
      background: #F9FAFB;
    }
    
    .cfm-tasks-title {
      font-size: 14px;
      font-weight: 700;
      color: #374151;
      margin: 0;
    }
    
    .cfm-all-del-btn {
      padding: 5px 12px;
      font-size: 12px;
      font-weight: 600;
      color: var(--cfm-red);
      background: #fff;
      border: 1.5px solid var(--cfm-red);
      border-radius: 5px;
      cursor: pointer;
      font-family: sans-serif;
      transition: background 0.15s;
    }
    .cfm-all-del-btn:hover:not(:disabled) { background: var(--cfm-red-light); }
    .cfm-all-del-btn:disabled { color: #9CA3AF; border-color: #D1D5DB; cursor: not-allowed; }
    
    .cfm-task-list { list-style: none; margin: 0; padding: 0; }
    
    .cfm-task-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px 16px;
      border-bottom: 1px solid var(--cfm-border);
      transition: opacity 0.3s, transform 0.3s;
    }
    .cfm-task-item:last-child { border-bottom: none; }
    .cfm-task-item.is-removing { opacity: 0; transform: translateX(16px); }
    
    .cfm-task-title { font-size: 14px; color: #374151; }
    
    .cfm-del-btn {
      padding: 4px 10px;
      font-size: 12px;
      font-weight: 500;
      color: var(--cfm-red);
      background: #fff;
      border: 1px solid var(--cfm-border);
      border-radius: 4px;
      cursor: pointer;
      font-family: sans-serif;
      transition: background 0.15s, border-color 0.15s;
    }
    .cfm-del-btn:hover { background: var(--cfm-red-light); border-color: var(--cfm-red); }
    
    .cfm-empty-msg {
      text-align: center;
      color: #9CA3AF;
      font-size: 14px;
      padding: 20px;
      margin: 0;
    }
    
    /* ================================================================
       操作ログ
    ================================================================ */
    .cfm-log {
      background: #F9FAFB;
      border: 1.5px solid var(--cfm-border);
      border-radius: var(--cfm-radius);
      overflow: hidden;
      max-width: 560px;
    }
    
    .cfm-log-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 10px 14px;
      border-bottom: 1px solid var(--cfm-border);
    }
    
    .cfm-log-label {
      font-size: 12px;
      font-weight: 700;
      color: var(--cfm-gray);
      letter-spacing: 0.04em;
    }
    
    /* サンプルソース用リセットボタン */
    .cfm-reset-btn {
      padding: 4px 12px;
      font-size: 12px;
      color: #5A6A7A;
      background: #fff;
      border: 1.5px solid #D0D7E0;
      border-radius: 5px;
      cursor: pointer;
      font-family: sans-serif;
      transition: background 0.15s;
    }
    .cfm-reset-btn:hover { background: #F4F6F9; }
    
    .cfm-log-list { list-style: none; margin: 0; padding: 8px 0; max-height: 140px; overflow-y: auto; }
    
    .cfm-log-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 4px 14px;
      font-size: 13px;
      color: #374151;
    }
    
    .cfm-log-item time {
      font-size: 11px;
      color: #9CA3AF;
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }
    
    .cfm-log-empty {
      padding: 8px 14px;
      color: #9CA3AF;
      font-size: 13px;
      text-align: center;
    }
    
    /* ================================================================
       確認ダイアログ(共通)
    ================================================================ */
    .cfm-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.45);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1000;
      padding: 16px;
    }
    .cfm-overlay[hidden] { display: none; }
    
    .cfm-dialog {
      background: #fff;
      border-radius: var(--cfm-radius);
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
      width: 100%;
      max-width: 400px;
      overflow: hidden;
      animation: cfm-pop 0.15s ease-out;
    }
    
    @keyframes cfm-pop {
      from { transform: scale(0.95); opacity: 0; }
      to   { transform: scale(1);    opacity: 1; }
    }
    
    /* 危険操作ダイアログ: 赤いトップボーダーでノーマルと区別 */
    .cfm-dialog--danger { border-top: 4px solid var(--cfm-red); }
    
    .cfm-dialog-header { padding: 20px 20px 0; }
    
    .cfm-dialog-title { font-size: 16px; font-weight: 700; color: #111827; margin: 0; }
    .cfm-dialog--danger .cfm-dialog-title { color: var(--cfm-red-dark); }
    
    .cfm-dialog-body { padding: 12px 20px 20px; }
    
    .cfm-dialog-msg { font-size: 14px; color: #374151; line-height: 1.65; margin: 0; }
    
    /* テキスト入力(Pattern 3) */
    .cfm-input-wrap { margin-top: 14px; }
    .cfm-input-label { display: block; font-size: 12px; color: var(--cfm-gray); margin-bottom: 6px; }
    .cfm-input {
      width: 100%;
      padding: 8px 12px;
      font-size: 14px;
      font-family: monospace;
      letter-spacing: 0.05em;
      border: 1.5px solid var(--cfm-border);
      border-radius: 6px;
      outline: none;
      box-sizing: border-box;
    }
    .cfm-input:focus { border-color: var(--cfm-red); box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15); }
    
    .cfm-dialog-footer {
      padding: 0 20px 20px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
    }
    
    .cfm-cancel-btn {
      padding: 8px 18px;
      font-size: 14px;
      font-weight: 500;
      color: #374151;
      background: #fff;
      border: 1.5px solid var(--cfm-border);
      border-radius: 6px;
      cursor: pointer;
      font-family: sans-serif;
      transition: background 0.15s;
    }
    .cfm-cancel-btn:hover { background: #F9FAFB; }
    
    .cfm-confirm-btn {
      padding: 8px 18px;
      font-size: 14px;
      font-weight: 600;
      color: #fff;
      background: var(--cfm-red);
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-family: sans-serif;
      transition: background 0.15s;
    }
    .cfm-confirm-btn:hover:not(:disabled) { background: var(--cfm-red-dark); }
    .cfm-confirm-btn:disabled { background: #D1D5DB; cursor: not-allowed; }
    .cfm-dialog--danger .cfm-confirm-btn { background: var(--cfm-red-dark); }
    .cfm-dialog--danger .cfm-confirm-btn:hover:not(:disabled) { background: #B91C1C; }
    // =========================================
    // タスクデータ(初期値)
    // =========================================
    var TASKS_INITIAL = [
      { id: 1, title: '企画書を作成する' },
      { id: 2, title: 'ミーティングの準備' },
      { id: 3, title: 'レポートを提出する' },
      { id: 4, title: 'コードレビュー' }
    ];
    
    // 現在のダイアログ確定コールバック
    var pendingCallback = null;
    // テキスト入力確認モードかどうか
    var needsInput = false;
    
    // =========================================
    // DOM 参照
    // =========================================
    var overlay    = document.getElementById('cfm-overlay');
    var dialog     = document.getElementById('cfm-dialog');
    var dialogTitle = document.getElementById('cfm-dialog-title');
    var dialogMsg  = document.getElementById('cfm-dialog-msg');
    var confirmBtn = document.getElementById('cfm-confirm-btn');
    var cancelBtn  = document.getElementById('cfm-cancel-btn');
    var inputWrap  = document.getElementById('cfm-input-wrap');
    var textInput  = document.getElementById('cfm-input');
    
    // =========================================
    // 初期化 / リセット
    // =========================================
    function resetDemo() {
      // プロフィールを元に戻す
      var profile = document.getElementById('cfm-profile');
      profile.classList.remove('is-deleted');
      document.getElementById('cfm-account-del-btn').disabled = false;
    
      // タスクリストを再生成
      renderTasks();
    
      // 全件削除ボタンを有効化
      document.getElementById('cfm-all-del-btn').disabled = false;
    
      // ログをリセット
      var logList = document.getElementById('cfm-log-list');
      logList.innerHTML = '<li class="cfm-log-empty">操作するとここにログが表示されます</li>';
    }
    
    // =========================================
    // タスクリストの生成
    // =========================================
    function renderTasks() {
      var list = document.getElementById('cfm-task-list');
      list.innerHTML = '';
      TASKS_INITIAL.forEach(function(task) {
        list.appendChild(createTaskItem(task));
      });
      updateEmptyState();
    }
    
    function createTaskItem(task) {
      var li = document.createElement('li');
      li.className = 'cfm-task-item';
    
      var span = document.createElement('span');
      span.className = 'cfm-task-title';
      span.textContent = task.title;
    
      var btn = document.createElement('button');
      btn.className = 'cfm-del-btn';
      btn.type = 'button';
      btn.textContent = '削除';
      btn.setAttribute('aria-label', task.title + ' を削除');
    
      // クロージャーで対応するタスクデータを保持する
      btn.onclick = function() {
        openDialog({
          title: 'タスクを削除しますか?',
          msg: '「' + task.title + '」を削除します。この操作は取り消せません。',
          type: 'normal',
          confirmText: '削除する',
          onConfirm: function() { deleteTask(li, task.title); }
        });
      };
    
      li.appendChild(span);
      li.appendChild(btn);
      return li;
    }
    
    // =========================================
    // ダイアログの開閉
    // =========================================
    function openDialog(opts) {
      needsInput = !!opts.hasInput;
      pendingCallback = opts.onConfirm;
    
      dialogTitle.textContent = opts.title;
      dialogMsg.textContent = opts.msg;
      confirmBtn.textContent = opts.confirmText;
    
      // ダイアログの種類でスタイルを切り替える
      dialog.className = 'cfm-dialog' + (opts.type === 'danger' ? ' cfm-dialog--danger' : '');
    
      // テキスト入力エリアの表示/非表示
      if (needsInput) {
        inputWrap.hidden = false;
        textInput.value = '';
        confirmBtn.disabled = true;
        textInput.oninput = function() {
          confirmBtn.disabled = (textInput.value !== 'DELETE');
        };
      } else {
        inputWrap.hidden = true;
        confirmBtn.disabled = false;
        textInput.oninput = null;
      }
    
      overlay.hidden = false;
      cancelBtn.focus();
    }
    
    function execConfirm() {
      if (pendingCallback) pendingCallback();
      overlay.hidden = true;
      pendingCallback = null;
    }
    
    function cancelDialog() {
      overlay.hidden = true;
      pendingCallback = null;
      addLog('キャンセルしました');
    }
    
    // =========================================
    // ボタン・キー操作
    // =========================================
    cancelBtn.addEventListener('click', cancelDialog);
    
    confirmBtn.addEventListener('click', execConfirm);
    
    // ESCキーでキャンセル
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && !overlay.hidden) cancelDialog();
    });
    
    // Enterキーで確定(テキスト入力なしのパターンのみ)
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' && !overlay.hidden && !needsInput && !confirmBtn.disabled) {
        execConfirm();
      }
    });
    
    // 全件削除ボタン
    document.getElementById('cfm-all-del-btn').addEventListener('click', function() {
      var items = document.querySelectorAll('.cfm-task-item');
      if (items.length === 0) return;
      openDialog({
        title: '全件削除しますか?',
        msg: 'すべてのタスク(' + items.length + '件)を削除します。この操作は取り消せません。',
        type: 'danger',
        confirmText: 'すべて削除する',
        onConfirm: deleteAllTasks
      });
    });
    
    // アカウント削除ボタン
    document.getElementById('cfm-account-del-btn').addEventListener('click', function() {
      openDialog({
        title: 'アカウントを削除しますか?',
        msg: 'この操作は取り消せません。確認のため「DELETE」と入力してください。',
        type: 'danger',
        confirmText: 'アカウントを削除する',
        hasInput: true,
        onConfirm: deleteAccount
      });
    });
    
    // =========================================
    // 削除アクション
    // =========================================
    function deleteTask(li, title) {
      li.classList.add('is-removing');
      // CSS transition(300ms)が完了してから DOM から削除する
      setTimeout(function() {
        li.remove();
        updateEmptyState();
      }, 300);
      addLog('「' + title + '」を削除しました');
    }
    
    function deleteAllTasks() {
      var items = document.querySelectorAll('.cfm-task-item');
      var count = items.length;
      items.forEach(function(item) { item.classList.add('is-removing'); });
      setTimeout(function() {
        document.getElementById('cfm-task-list').innerHTML = '';
        updateEmptyState();
        document.getElementById('cfm-all-del-btn').disabled = true;
      }, 300);
      addLog('タスクをすべて削除しました(' + count + '件)');
    }
    
    function deleteAccount() {
      var profile = document.getElementById('cfm-profile');
      profile.classList.add('is-deleted');
      document.getElementById('cfm-account-del-btn').disabled = true;
      addLog('アカウントを削除しました');
    }
    
    // =========================================
    // ユーティリティ
    // =========================================
    function updateEmptyState() {
      var items = document.querySelectorAll('.cfm-task-item');
      document.getElementById('cfm-empty-msg').hidden = (items.length > 0);
    }
    
    function addLog(msg) {
      var logList = document.getElementById('cfm-log-list');
    
      // 初回ログ時にプレースホルダーを消す
      var placeholder = logList.querySelector('.cfm-log-empty');
      if (placeholder) placeholder.remove();
    
      var li = document.createElement('li');
      li.className = 'cfm-log-item';
    
      var timeEl = document.createElement('time');
      var now = new Date();
      var hh = String(now.getHours()).padStart(2, '0');
      var mm = String(now.getMinutes()).padStart(2, '0');
      var ss = String(now.getSeconds()).padStart(2, '0');
      timeEl.textContent = hh + ':' + mm + ':' + ss;
    
      var msgEl = document.createElement('span');
      msgEl.textContent = msg;
    
      li.appendChild(timeEl);
      li.appendChild(msgEl);
    
      // 新しいログを先頭に追加する
      logList.insertBefore(li, logList.firstChild);
    }
    
    // 初期化
    renderTasks();

    AI用プロンプト

    各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

    ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や要件の追記など、用途に合わせて変えて使うのがおすすめです。

    ※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

    💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

    Pattern 1 — 個別削除(ノーマル確認)

    # 確認ダイアログ(個別削除)作成依頼
    
    ## 概要
    リスト上の各アイテムを削除するとき、確認ダイアログを挟んでから削除するUIを実装してください。
    
    ## 要件
    - タスクを4件リスト表示する(データはJavaScript配列で定義)
    - 各タスク行に「削除」ボタンを設置する
    - 「削除」ボタンを押すと確認ダイアログが開く
    - ダイアログには「タスクを削除しますか?」のタイトルと対象タスク名を含むメッセージを表示する
    - 「削除する」ボタン(赤)と「キャンセル」ボタンを並べる
    - 確認後、対象タスクをフェードアウトして削除する
    - Escキーとキャンセルボタンでダイアログを閉じる、Enterキーで確定できる
    - 操作結果を画面下のログ欄にタイムスタンプ付きで表示する
    
    ## 技術仕様
    - HTML / CSS / バニラJavaScript で実装
    - 外部ライブラリ:なし
    - レスポンシブ対応:必要
    
    ## 動作詳細
    ダイアログは position: fixed でページ全体を覆うオーバーレイ(半透明の黒背景)の中央に表示する。
    削除ボタンは赤(#EF4444)。
    タスク削除時は opacity: 0 + translateX でフェードアウトさせてから DOM から削除する(300ms)。
    テキストの出力はすべて textContent を使いXSS対策を徹底する。
    
    ## 出力形式
    HTML・CSS・JavaScriptを分けて出力してください。
    各ファイルは単独でコピー&ペーストして使えるよう記述してください。

    Pattern 2 — 全件削除(危険操作確認)

    # 確認ダイアログ(全件削除・危険操作)作成依頼
    
    ## 概要
    全件削除など取り消しできない危険な操作の前に、強調スタイルの確認ダイアログを表示するUIを実装してください。
    
    ## 要件
    - タスクを4件リスト表示する(データはJavaScript配列で定義)
    - 「全件削除」ボタンを設置し、クリックで危険操作ダイアログを開く
    - 危険操作ダイアログは上部に赤いボーダー(4px solid #EF4444)を表示してノーマルと視覚的に区別する
    - タイトルを赤文字で「全件削除しますか?」と表示し、現在の件数(〇件)をメッセージに含める
    - 「すべて削除する」ボタン(濃い赤 #DC2626)と「キャンセル」ボタンを並べる
    - 確認後、すべてのタスクをフェードアウトして削除し「タスクがありません」を表示する
    - 操作結果を操作ログ欄に記録する
    
    ## 技術仕様
    - HTML / CSS / バニラJavaScript で実装
    - 外部ライブラリ:なし
    - レスポンシブ対応:必要
    
    ## 動作詳細
    ダイアログHTMLは1つだけ用意し、openDialog() 関数に type: 'normal'/'danger' を渡してスタイルを切り替える。
    危険ダイアログは dialog.className に 'cfm-dialog--danger' を追加してCSSを切り替える。
    テキストの出力はすべて textContent を使いXSS対策を徹底する。
    
    ## 出力形式
    HTML・CSS・JavaScriptを分けて出力してください。
    各ファイルは単独でコピー&ペーストして使えるよう記述してください。

    Pattern 3 — アカウント削除(テキスト入力確認)

    # 確認ダイアログ(テキスト入力確認)作成依頼
    
    ## 概要
    取り消し不能な重要操作の前に「DELETE」と入力しないと確定できないダイアログを実装してください。
    
    ## 要件
    - ユーザープロフィール(名前・メール・アバター)をカード表示する
    - 「アカウントを削除」ボタンを設置し、クリックでテキスト入力確認ダイアログを開く
    - ダイアログ内にテキスト入力欄を設置し、「DELETE」と正確に入力するまで確定ボタンを disabled にする
    - 入力が「DELETE」と一致した瞬間にボタンを有効化する(大文字小文字を区別する)
    - 確認後、プロフィールカードを「削除済み」状態(名前に取り消し線・アバター半透明)に変化させる
    - Escキーでキャンセルできる。Enterキーによる確定は無効にする(テキスト入力中の誤操作を防ぐため)
    
    ## 技術仕様
    - HTML / CSS / バニラJavaScript で実装
    - 外部ライブラリ:なし
    - レスポンシブ対応:必要
    
    ## 動作詳細
    input の oninput イベントで入力値が 'DELETE' と一致するかチェックし、confirmBtn.disabled を切り替える。
    ダイアログを開くたびに入力欄をクリアし確定ボタンを disabled に戻す。
    テキストの出力はすべて textContent を使いXSS対策を徹底する。
    
    ## 出力形式
    HTML・CSS・JavaScriptを分けて出力してください。
    各ファイルは単独でコピー&ペーストして使えるよう記述してください。