Confirm Dialog — Type to Confirm HTML CSS JS Snippet

Confirm Dialog · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Type-to-confirm guard
checkPhrase enables Delete only when the input exactly matches the required phrase, forcing deliberate intent.
Markup as source of truth
The required phrase is read from the on-screen <code>, so changing the displayed name updates the check automatically.
Match feedback
The input border turns green on a match, giving immediate confirmation that the name is correct before clicking.
Blurred modal overlay
backdrop-filter: blur dims and frosts the page; the dialog scales/slides in with transform+opacity for a smooth, export-safe entrance.
Three dismissal paths
Cancel, backdrop click (guarded by e.target === overlay), and Escape all route through closeDialog.
Deleting→done sequence
confirmDelete transitions the button through "Deleting…" and "✓ Deleted", a clear async lifecycle stand-in.
Re-entrancy lock
A busy flag blocks dismissal and repeat clicks during the delete sequence so the action cannot double-fire.
Auto-focus and reset
Opening focuses the input; closing clears the field, match style, and button label so the guard is fresh on reopen.

About this UI Snippet

Confirm Dialog — Type-to-Confirm Guard, Backdrop Blur & Deleting→Done State

Screenshot of the Confirm Dialog snippet rendered live

For genuinely destructive actions — deleting a project, dropping a database, closing an account — a plain "Are you sure? [OK]" is too easy to click through on autopilot. The stronger pattern, used by GitHub, Stripe, and Vercel, is type-to-confirm: the user must type the exact name of the thing being destroyed before the confirm button unlocks. That deliberate friction is the point. This snippet implements it in plain HTML, CSS, and vanilla JavaScript: a type-to-confirm guard, a blurred modal overlay, the standard dismissal behaviours, and a deleting→done state.

The type-to-confirm guard

The Delete button starts disabled. checkPhrase runs on every keystroke and compares the trimmed input against the required PHRASE (read from the on-screen <code> so the markup is the source of truth). Only an exact match enables the button and turns the input border green. This forces the user to consciously read and reproduce the resource's name, which all but eliminates accidental deletions and "muscle-memory" confirmations — the entire reason the pattern exists.

A real modal, dismissed the expected ways

The dialog uses role="alertdialog" and aria-modal, with a blurred, dimmed overlay (backdrop-filter: blur). It animates in by scaling and sliding from scale(.94) to scale(1) with opacity — transform/opacity only, so it is smooth and exports cleanly. It can be dismissed three ways: the Cancel button, clicking the backdrop (overlayClick checks e.target === overlay so clicks inside the dialog do not close it), and pressing Escape. Every dismissal path runs closeDialog, which also resets the input and re-disables the button so the guard is fresh next time.

Deleting → done state with a re-entrancy lock

On confirm, confirmDelete switches the button to "Deleting…", then to a green "✓ Deleted", then closes — standing in for an async request. A busy flag locks the dialog during this sequence so Escape, the backdrop, and repeat clicks cannot interrupt or double-fire the action mid-delete. Once finished, closeDialog fully resets the dialog for reuse.

Focus and reset

Opening the dialog focuses the input after the animation so the user can start typing immediately, and closing clears the field, the match style, and the button label — so reopening always presents a clean, locked confirm.

Wire your real delete call into confirmDelete (before the success state) and you have a production-grade safeguard. Pair this with a slide to confirm for an alternative deliberate gesture, a snackbar with undo for reversible deletes, or a modal for non-destructive dialogs.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA red "Delete project" button appears; clicking it opens a blurred modal asking you to type the project name.
  2. 2
    See the locked confirmThe Delete button inside the dialog is disabled — you cannot click it until you prove intent.
  3. 3
    Type the phraseType aurora-api exactly — the input border turns green and the Delete button unlocks the moment it matches.
  4. 4
    ConfirmClick Delete — it shows "Deleting…", then a green "✓ Deleted", then the dialog closes.
  5. 5
    Dismiss itCancel, click the dimmed backdrop, or press Escape to close without deleting; the guard resets for next time.
  6. 6
    Wire your actionPut your real delete API call in confirmDelete before the success state — it only runs once the typed name matches.

Real-world uses

Common Use Cases

Delete project / repo / database
The canonical GitHub/Vercel-style guard for irreversible resource deletion in dashboards and admin tools.
Account closure and data wipes
Require typing the account email or "DELETE" before permanently closing an account or erasing data.
Production / billing changes
Guard high-stakes settings (cancel subscription, rotate keys, drop a table) where an accidental click is costly. Often paired with a subscription widget.
Bulk destructive operations
Confirm mass deletes or irreversible migrations; pair with a snackbar with undo where reversibility is possible.
Transfer and ownership changes
Type the target name to confirm transferring ownership of a project or organisation.
Alternative to slide-to-confirm
Where typing fits better than a gesture; compare with a slide to confirm control for touch-first flows.

Got questions?

Frequently Asked Questions

Put your API call inside confirmDelete before showing the success state: keep the busy lock set, await the request, then switch to "✓ Deleted" and close on success. On failure, clear busy, restore the button label, and show an error message instead of closing — so the user can retry without losing the dialog.

The phrase is read from the #cdPhrase element's text, so render the actual resource name into both that <code> and the warning text when you open the dialog. checkPhrase compares against whatever is shown, so per-item dialogs (deleting "billing-prod" vs "aurora-api") work with no code change.

By default this matches exactly (case-sensitive), which is the safest and matches GitHub's behaviour — it forces careful reproduction. If your resource names are case-insensitive, compare with .toLowerCase() on both sides. Keep the trim (already applied) so trailing spaces from autocomplete do not block a correct match.

It uses role="alertdialog" and aria-modal with an aria-labelledby pointing at the title. For full compliance, trap focus inside the dialog while open (cycle Tab between the input and buttons), return focus to the trigger on close, and ensure Escape closes it (it does). The green match state should be paired with the unlocked button, not colour alone, so the cue is not purely visual.

In React, hold open, the typed value, and a busy flag in useState; derive the button's disabled state from value === phrase, and add an Escape useEffect. In Vue, use refs with v-model and a computed match. In Angular, track state on the component and bind [disabled]. The overlay/scale-in CSS and guard logic port unchanged.

Confirm Dialog — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Confirm Dialog</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    
    .cd-trigger{display:inline-flex;align-items:center;gap:8px;background:#fff;color:#dc2626;border:1.5px solid #fecaca;border-radius:11px;padding:11px 18px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s}
    .cd-trigger:hover{background:#fef2f2;border-color:#fca5a5}
    
    .cd-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s;z-index:50}
    .cd-overlay.show{opacity:1;visibility:visible}
    
    .cd-dialog{background:#fff;border-radius:18px;padding:26px;width:100%;max-width:380px;box-shadow:0 25px 60px rgba(0,0,0,.3);transform:scale(.94) translateY(10px);transition:transform .25s cubic-bezier(.2,1.1,.4,1)}
    .cd-overlay.show .cd-dialog{transform:scale(1) translateY(0)}
    
    .cd-icon{width:48px;height:48px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin-bottom:14px}
    .cd-title{font-size:18px;font-weight:800;color:#1e293b;margin-bottom:8px}
    .cd-text{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:18px}
    .cd-text strong{color:#1e293b;font-weight:700}
    
    .cd-label{display:block;font-size:12px;color:#475569;margin-bottom:7px}
    .cd-label code{background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px}
    .cd-input{width:100%;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:14px;color:#1e293b;outline:none;font-family:ui-monospace,monospace;transition:border-color .15s,box-shadow .15s;margin-bottom:18px}
    .cd-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
    .cd-input.match{border-color:#10b981}
    
    .cd-actions{display:flex;gap:10px}
    .cd-cancel,.cd-confirm{flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s}
    .cd-cancel{background:#f1f5f9;color:#475569}
    .cd-cancel:hover{background:#e2e8f0}
    .cd-confirm{background:#dc2626;color:#fff}
    .cd-confirm:hover:not(:disabled){background:#b91c1c}
    .cd-confirm:disabled{background:#fca5a5;cursor:not-allowed;opacity:.7}
    .cd-confirm.done{background:#10b981!important}
  </style>
</head>
<body>
  <div class="cd-page">
    <button class="cd-trigger" onclick="openDialog()">
      <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
      Delete project
    </button>
  
    <div class="cd-overlay" id="cdOverlay" onclick="overlayClick(event)">
      <div class="cd-dialog" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
        <div class="cd-icon">
          <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
        </div>
        <h2 class="cd-title" id="cdTitle">Delete this project?</h2>
        <p class="cd-text">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
  
        <label class="cd-label">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
        <input class="cd-input" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" oninput="checkPhrase(this)">
  
        <div class="cd-actions">
          <button class="cd-cancel" onclick="closeDialog()">Cancel</button>
          <button class="cd-confirm" id="cdConfirm" onclick="confirmDelete()" disabled>Delete project</button>
        </div>
      </div>
    </div>
  </div>
  <script>
    var overlay = document.getElementById('cdOverlay');
    var input = document.getElementById('cdInput');
    var confirm = document.getElementById('cdConfirm');
    var PHRASE = document.getElementById('cdPhrase').textContent;
    var busy = false;
    
    function openDialog() {
      overlay.classList.add('show');
      setTimeout(function () { input.focus(); }, 120);
    }
    
    function closeDialog() {
      if (busy) return;
      overlay.classList.remove('show');
      input.value = '';
      input.classList.remove('match');
      confirm.disabled = true;
      confirm.classList.remove('done');
      confirm.textContent = 'Delete project';
    }
    
    function overlayClick(e) {
      if (e.target === overlay) closeDialog();
    }
    
    function checkPhrase(el) {
      var match = el.value.trim() === PHRASE;
      confirm.disabled = !match;
      el.classList.toggle('match', match);
    }
    
    function confirmDelete() {
      if (confirm.disabled || busy) return;
      busy = true;
      confirm.textContent = 'Deleting…';
      setTimeout(function () {
        confirm.textContent = '✓ Deleted';
        confirm.classList.add('done');
        setTimeout(function () { busy = false; closeDialog(); }, 1000);
      }, 850);
    }
    
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeDialog();
    });
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Confirm Dialog</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
    }

    .cd-overlay.show {
      opacity:1;visibility:visible
    }

    .cd-overlay.show .cd-dialog {
      transform:scale(1) translateY(0)
    }

    .cd-text strong {
      color:#1e293b;font-weight:700
    }

    .cd-label code {
      background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px
    }

    .cd-input.match {
      border-color:#10b981
    }

    .cd-cancel,.cd-confirm {
      flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s
    }

    .cd-confirm:hover:not(:disabled) {
      background:#b91c1c
    }

    .cd-confirm:disabled {
      background:#fca5a5;cursor:not-allowed;opacity:.7
    }

    .cd-confirm.done {
      background:#10b981!important
    }
  </style>
</head>
<body>
  <div class="cd-page">
    <button class="inline-flex items-center gap-2 bg-[#fff] text-[#dc2626] border rounded-[11px] py-[11px] px-[18px] text-sm font-bold cursor-pointer font-[inherit] [transition:background_.15s,border-color_.15s] hover:bg-[#fef2f2] hover:border-[#fca5a5]" onclick="openDialog()">
      <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
      Delete project
    </button>
  
    <div class="cd-overlay fixed inset-0 bg-[rgba(15,23,42,.5)] [backdrop-filter:blur(4px)] flex items-center justify-center p-5 opacity-0 invisible [transition:opacity_.25s,visibility_.25s] z-50" id="cdOverlay" onclick="overlayClick(event)">
      <div class="cd-dialog bg-[#fff] rounded-[18px] p-[26px] w-full max-w-[380px] shadow-[0_25px_60px_rgba(0,0,0,.3)] [transform:scale(.94)_translateY(10px)] [transition:transform_.25s_cubic-bezier(.2,1.1,.4,1)]" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
        <div class="w-12 h-12 rounded-full bg-[#fee2e2] flex items-center justify-center mb-3.5">
          <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
        </div>
        <h2 class="text-lg font-extrabold text-[#1e293b] mb-2" id="cdTitle">Delete this project?</h2>
        <p class="cd-text text-[13px] text-[#64748b] leading-[1.55] mb-[18px]">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
  
        <label class="cd-label block text-xs text-[#475569] mb-[7px]">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
        <input class="cd-input w-full py-2.5 px-3 border rounded-[10px] text-sm text-[#1e293b] outline-none font-mono [transition:border-color_.15s,box-shadow_.15s] mb-[18px] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" oninput="checkPhrase(this)">
  
        <div class="flex gap-2.5">
          <button class="cd-cancel bg-[#f1f5f9] text-[#475569] hover:bg-[#e2e8f0]" onclick="closeDialog()">Cancel</button>
          <button class="cd-confirm bg-[#dc2626] text-[#fff]" id="cdConfirm" onclick="confirmDelete()" disabled>Delete project</button>
        </div>
      </div>
    </div>
  </div>
  <script>
    var overlay = document.getElementById('cdOverlay');
    var input = document.getElementById('cdInput');
    var confirm = document.getElementById('cdConfirm');
    var PHRASE = document.getElementById('cdPhrase').textContent;
    var busy = false;
    
    function openDialog() {
      overlay.classList.add('show');
      setTimeout(function () { input.focus(); }, 120);
    }
    
    function closeDialog() {
      if (busy) return;
      overlay.classList.remove('show');
      input.value = '';
      input.classList.remove('match');
      confirm.disabled = true;
      confirm.classList.remove('done');
      confirm.textContent = 'Delete project';
    }
    
    function overlayClick(e) {
      if (e.target === overlay) closeDialog();
    }
    
    function checkPhrase(el) {
      var match = el.value.trim() === PHRASE;
      confirm.disabled = !match;
      el.classList.toggle('match', match);
    }
    
    function confirmDelete() {
      if (confirm.disabled || busy) return;
      busy = true;
      confirm.textContent = 'Deleting…';
      setTimeout(function () {
        confirm.textContent = '✓ Deleted';
        confirm.classList.add('done');
        setTimeout(function () { busy = false; closeDialog(); }, 1000);
      }, 850);
    }
    
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeDialog();
    });
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to ConfirmDialog.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}

.cd-trigger{display:inline-flex;align-items:center;gap:8px;background:#fff;color:#dc2626;border:1.5px solid #fecaca;border-radius:11px;padding:11px 18px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s}
.cd-trigger:hover{background:#fef2f2;border-color:#fca5a5}

.cd-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s;z-index:50}
.cd-overlay.show{opacity:1;visibility:visible}

.cd-dialog{background:#fff;border-radius:18px;padding:26px;width:100%;max-width:380px;box-shadow:0 25px 60px rgba(0,0,0,.3);transform:scale(.94) translateY(10px);transition:transform .25s cubic-bezier(.2,1.1,.4,1)}
.cd-overlay.show .cd-dialog{transform:scale(1) translateY(0)}

.cd-icon{width:48px;height:48px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin-bottom:14px}
.cd-title{font-size:18px;font-weight:800;color:#1e293b;margin-bottom:8px}
.cd-text{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:18px}
.cd-text strong{color:#1e293b;font-weight:700}

.cd-label{display:block;font-size:12px;color:#475569;margin-bottom:7px}
.cd-label code{background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px}
.cd-input{width:100%;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:14px;color:#1e293b;outline:none;font-family:ui-monospace,monospace;transition:border-color .15s,box-shadow .15s;margin-bottom:18px}
.cd-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.cd-input.match{border-color:#10b981}

.cd-actions{display:flex;gap:10px}
.cd-cancel,.cd-confirm{flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s}
.cd-cancel{background:#f1f5f9;color:#475569}
.cd-cancel:hover{background:#e2e8f0}
.cd-confirm{background:#dc2626;color:#fff}
.cd-confirm:hover:not(:disabled){background:#b91c1c}
.cd-confirm:disabled{background:#fca5a5;cursor:not-allowed;opacity:.7}
.cd-confirm.done{background:#10b981!important}
`;

export default function ConfirmDialog() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var overlay = document.getElementById('cdOverlay');
    var input = document.getElementById('cdInput');
    var confirm = document.getElementById('cdConfirm');
    var PHRASE = document.getElementById('cdPhrase').textContent;
    var busy = false;
    
    function openDialog() {
      overlay.classList.add('show');
      setTimeout(function () { input.focus(); }, 120);
    }
    
    function closeDialog() {
      if (busy) return;
      overlay.classList.remove('show');
      input.value = '';
      input.classList.remove('match');
      confirm.disabled = true;
      confirm.classList.remove('done');
      confirm.textContent = 'Delete project';
    }
    
    function overlayClick(e) {
      if (e.target === overlay) closeDialog();
    }
    
    function checkPhrase(el) {
      var match = el.value.trim() === PHRASE;
      confirm.disabled = !match;
      el.classList.toggle('match', match);
    }
    
    function confirmDelete() {
      if (confirm.disabled || busy) return;
      busy = true;
      confirm.textContent = 'Deleting…';
      setTimeout(function () {
        confirm.textContent = '✓ Deleted';
        confirm.classList.add('done');
        setTimeout(function () { busy = false; closeDialog(); }, 1000);
      }, 850);
    }
    
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeDialog();
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof openDialog === 'function') window.openDialog = openDialog;
    if (typeof overlayClick === 'function') window.overlayClick = overlayClick;
    if (typeof checkPhrase === 'function') window.checkPhrase = checkPhrase;
    if (typeof closeDialog === 'function') window.closeDialog = closeDialog;
    if (typeof confirmDelete === 'function') window.confirmDelete = confirmDelete;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="cd-page">
        <button className="cd-trigger" onClick={(event) => { openDialog() }}>
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
          Delete project
        </button>
      
        <div className="cd-overlay" id="cdOverlay" onClick={(event) => { overlayClick(event) }}>
          <div className="cd-dialog" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
            <div className="cd-icon">
              <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
            </div>
            <h2 className="cd-title" id="cdTitle">Delete this project?</h2>
            <p className="cd-text">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
      
            <label className="cd-label">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
            <input className="cd-input" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" onInput={(event) => { checkPhrase(event.currentTarget) }} />
      
            <div className="cd-actions">
              <button className="cd-cancel" onClick={(event) => { closeDialog() }}>Cancel</button>
              <button className="cd-confirm" id="cdConfirm" onClick={(event) => { confirmDelete() }} defaultDisabled>Delete project</button>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function ConfirmDialog() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var overlay = document.getElementById('cdOverlay');
    var input = document.getElementById('cdInput');
    var confirm = document.getElementById('cdConfirm');
    var PHRASE = document.getElementById('cdPhrase').textContent;
    var busy = false;
    
    function openDialog() {
      overlay.classList.add('show');
      setTimeout(function () { input.focus(); }, 120);
    }
    
    function closeDialog() {
      if (busy) return;
      overlay.classList.remove('show');
      input.value = '';
      input.classList.remove('match');
      confirm.disabled = true;
      confirm.classList.remove('done');
      confirm.textContent = 'Delete project';
    }
    
    function overlayClick(e) {
      if (e.target === overlay) closeDialog();
    }
    
    function checkPhrase(el) {
      var match = el.value.trim() === PHRASE;
      confirm.disabled = !match;
      el.classList.toggle('match', match);
    }
    
    function confirmDelete() {
      if (confirm.disabled || busy) return;
      busy = true;
      confirm.textContent = 'Deleting…';
      setTimeout(function () {
        confirm.textContent = '✓ Deleted';
        confirm.classList.add('done');
        setTimeout(function () { busy = false; closeDialog(); }, 1000);
      }, 850);
    }
    
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeDialog();
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof openDialog === 'function') window.openDialog = openDialog;
    if (typeof overlayClick === 'function') window.overlayClick = overlayClick;
    if (typeof checkPhrase === 'function') window.checkPhrase = checkPhrase;
    if (typeof closeDialog === 'function') window.closeDialog = closeDialog;
    if (typeof confirmDelete === 'function') window.confirmDelete = confirmDelete;
  }, []);

  return (
    <>
      <style>{`
* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}

.cd-overlay.show {
  opacity:1;visibility:visible
}

.cd-overlay.show .cd-dialog {
  transform:scale(1) translateY(0)
}

.cd-text strong {
  color:#1e293b;font-weight:700
}

.cd-label code {
  background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px
}

.cd-input.match {
  border-color:#10b981
}

.cd-cancel,.cd-confirm {
  flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s
}

.cd-confirm:hover:not(:disabled) {
  background:#b91c1c
}

.cd-confirm:disabled {
  background:#fca5a5;cursor:not-allowed;opacity:.7
}

.cd-confirm.done {
  background:#10b981!important
}
      `}</style>
      <div className="cd-page">
        <button className="inline-flex items-center gap-2 bg-[#fff] text-[#dc2626] border rounded-[11px] py-[11px] px-[18px] text-sm font-bold cursor-pointer font-[inherit] [transition:background_.15s,border-color_.15s] hover:bg-[#fef2f2] hover:border-[#fca5a5]" onClick={(event) => { openDialog() }}>
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
          Delete project
        </button>
      
        <div className="cd-overlay fixed inset-0 bg-[rgba(15,23,42,.5)] [backdrop-filter:blur(4px)] flex items-center justify-center p-5 opacity-0 invisible [transition:opacity_.25s,visibility_.25s] z-50" id="cdOverlay" onClick={(event) => { overlayClick(event) }}>
          <div className="cd-dialog bg-[#fff] rounded-[18px] p-[26px] w-full max-w-[380px] shadow-[0_25px_60px_rgba(0,0,0,.3)] [transform:scale(.94)_translateY(10px)] [transition:transform_.25s_cubic-bezier(.2,1.1,.4,1)]" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
            <div className="w-12 h-12 rounded-full bg-[#fee2e2] flex items-center justify-center mb-3.5">
              <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
            </div>
            <h2 className="text-lg font-extrabold text-[#1e293b] mb-2" id="cdTitle">Delete this project?</h2>
            <p className="cd-text text-[13px] text-[#64748b] leading-[1.55] mb-[18px]">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
      
            <label className="cd-label block text-xs text-[#475569] mb-[7px]">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
            <input className="cd-input w-full py-2.5 px-3 border rounded-[10px] text-sm text-[#1e293b] outline-none font-mono [transition:border-color_.15s,box-shadow_.15s] mb-[18px] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" onInput={(event) => { checkPhrase(event.currentTarget) }} />
      
            <div className="flex gap-2.5">
              <button className="cd-cancel bg-[#f1f5f9] text-[#475569] hover:bg-[#e2e8f0]" onClick={(event) => { closeDialog() }}>Cancel</button>
              <button className="cd-confirm bg-[#dc2626] text-[#fff]" id="cdConfirm" onClick={(event) => { confirmDelete() }} defaultDisabled>Delete project</button>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="cd-page">
    <button class="cd-trigger" @click="openDialog()">
      <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
      Delete project
    </button>
  
    <div class="cd-overlay" id="cdOverlay" @click="overlayClick($event)">
      <div class="cd-dialog" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
        <div class="cd-icon">
          <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
        </div>
        <h2 class="cd-title" id="cdTitle">Delete this project?</h2>
        <p class="cd-text">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
  
        <label class="cd-label">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
        <input class="cd-input" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" @input="checkPhrase($event.currentTarget)">
  
        <div class="cd-actions">
          <button class="cd-cancel" @click="closeDialog()">Cancel</button>
          <button class="cd-confirm" id="cdConfirm" @click="confirmDelete()" disabled>Delete project</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

let overlay;
let input;
let confirm;
let PHRASE;
var busy = false;
function openDialog() {
  overlay.classList.add('show');
  setTimeout(function () { input.focus(); }, 120);
}
function closeDialog() {
  if (busy) return;
  overlay.classList.remove('show');
  input.value = '';
  input.classList.remove('match');
  confirm.disabled = true;
  confirm.classList.remove('done');
  confirm.textContent = 'Delete project';
}
function overlayClick(e) {
  if (e.target === overlay) closeDialog();
}
function checkPhrase(el) {
  var match = el.value.trim() === PHRASE;
  confirm.disabled = !match;
  el.classList.toggle('match', match);
}
function confirmDelete() {
  if (confirm.disabled || busy) return;
  busy = true;
  confirm.textContent = 'Deleting…';
  setTimeout(function () {
    confirm.textContent = '✓ Deleted';
    confirm.classList.add('done');
    setTimeout(function () { busy = false; closeDialog(); }, 1000);
  }, 850);
}

onMounted(() => {
  overlay = document.getElementById('cdOverlay');
  input = document.getElementById('cdInput');
  confirm = document.getElementById('cdConfirm');
  PHRASE = document.getElementById('cdPhrase').textContent;
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') closeDialog();
  });
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}

.cd-trigger{display:inline-flex;align-items:center;gap:8px;background:#fff;color:#dc2626;border:1.5px solid #fecaca;border-radius:11px;padding:11px 18px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s}
.cd-trigger:hover{background:#fef2f2;border-color:#fca5a5}

.cd-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s;z-index:50}
.cd-overlay.show{opacity:1;visibility:visible}

.cd-dialog{background:#fff;border-radius:18px;padding:26px;width:100%;max-width:380px;box-shadow:0 25px 60px rgba(0,0,0,.3);transform:scale(.94) translateY(10px);transition:transform .25s cubic-bezier(.2,1.1,.4,1)}
.cd-overlay.show .cd-dialog{transform:scale(1) translateY(0)}

.cd-icon{width:48px;height:48px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin-bottom:14px}
.cd-title{font-size:18px;font-weight:800;color:#1e293b;margin-bottom:8px}
.cd-text{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:18px}
.cd-text strong{color:#1e293b;font-weight:700}

.cd-label{display:block;font-size:12px;color:#475569;margin-bottom:7px}
.cd-label code{background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px}
.cd-input{width:100%;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:14px;color:#1e293b;outline:none;font-family:ui-monospace,monospace;transition:border-color .15s,box-shadow .15s;margin-bottom:18px}
.cd-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.cd-input.match{border-color:#10b981}

.cd-actions{display:flex;gap:10px}
.cd-cancel,.cd-confirm{flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s}
.cd-cancel{background:#f1f5f9;color:#475569}
.cd-cancel:hover{background:#e2e8f0}
.cd-confirm{background:#dc2626;color:#fff}
.cd-confirm:hover:not(:disabled){background:#b91c1c}
.cd-confirm:disabled{background:#fca5a5;cursor:not-allowed;opacity:.7}
.cd-confirm.done{background:#10b981!important}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-confirm-dialog',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="cd-page">
      <button class="cd-trigger" (click)="openDialog()">
        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
        Delete project
      </button>
    
      <div class="cd-overlay" id="cdOverlay" (click)="overlayClick($event)">
        <div class="cd-dialog" role="alertdialog" aria-modal="true" aria-labelledby="cdTitle">
          <div class="cd-icon">
            <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>
          </div>
          <h2 class="cd-title" id="cdTitle">Delete this project?</h2>
          <p class="cd-text">This permanently deletes <strong>aurora-api</strong> and all of its deployments, logs, and secrets. This action <strong>cannot be undone</strong>.</p>
    
          <label class="cd-label">Type <code id="cdPhrase">aurora-api</code> to confirm</label>
          <input class="cd-input" id="cdInput" type="text" placeholder="aurora-api" autocomplete="off" spellcheck="false" (input)="checkPhrase($event.currentTarget)">
    
          <div class="cd-actions">
            <button class="cd-cancel" (click)="closeDialog()">Cancel</button>
            <button class="cd-confirm" id="cdConfirm" (click)="confirmDelete()" disabled>Delete project</button>
          </div>
        </div>
      </div>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    
    .cd-trigger{display:inline-flex;align-items:center;gap:8px;background:#fff;color:#dc2626;border:1.5px solid #fecaca;border-radius:11px;padding:11px 18px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s}
    .cd-trigger:hover{background:#fef2f2;border-color:#fca5a5}
    
    .cd-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s;z-index:50}
    .cd-overlay.show{opacity:1;visibility:visible}
    
    .cd-dialog{background:#fff;border-radius:18px;padding:26px;width:100%;max-width:380px;box-shadow:0 25px 60px rgba(0,0,0,.3);transform:scale(.94) translateY(10px);transition:transform .25s cubic-bezier(.2,1.1,.4,1)}
    .cd-overlay.show .cd-dialog{transform:scale(1) translateY(0)}
    
    .cd-icon{width:48px;height:48px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin-bottom:14px}
    .cd-title{font-size:18px;font-weight:800;color:#1e293b;margin-bottom:8px}
    .cd-text{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:18px}
    .cd-text strong{color:#1e293b;font-weight:700}
    
    .cd-label{display:block;font-size:12px;color:#475569;margin-bottom:7px}
    .cd-label code{background:#f1f5f9;color:#dc2626;font-weight:700;padding:2px 6px;border-radius:5px;font-family:ui-monospace,monospace;font-size:12px}
    .cd-input{width:100%;padding:10px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:14px;color:#1e293b;outline:none;font-family:ui-monospace,monospace;transition:border-color .15s,box-shadow .15s;margin-bottom:18px}
    .cd-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
    .cd-input.match{border-color:#10b981}
    
    .cd-actions{display:flex;gap:10px}
    .cd-cancel,.cd-confirm{flex:1;padding:11px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s,opacity .15s}
    .cd-cancel{background:#f1f5f9;color:#475569}
    .cd-cancel:hover{background:#e2e8f0}
    .cd-confirm{background:#dc2626;color:#fff}
    .cd-confirm:hover:not(:disabled){background:#b91c1c}
    .cd-confirm:disabled{background:#fca5a5;cursor:not-allowed;opacity:.7}
    .cd-confirm.done{background:#10b981!important}
  `]
})
export class ConfirmDialogComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var overlay = document.getElementById('cdOverlay');
    var input = document.getElementById('cdInput');
    var confirm = document.getElementById('cdConfirm');
    var PHRASE = document.getElementById('cdPhrase').textContent;
    var busy = false;
    
    function openDialog() {
      overlay.classList.add('show');
      setTimeout(function () { input.focus(); }, 120);
    }
    
    function closeDialog() {
      if (busy) return;
      overlay.classList.remove('show');
      input.value = '';
      input.classList.remove('match');
      confirm.disabled = true;
      confirm.classList.remove('done');
      confirm.textContent = 'Delete project';
    }
    
    function overlayClick(e) {
      if (e.target === overlay) closeDialog();
    }
    
    function checkPhrase(el) {
      var match = el.value.trim() === PHRASE;
      confirm.disabled = !match;
      el.classList.toggle('match', match);
    }
    
    function confirmDelete() {
      if (confirm.disabled || busy) return;
      busy = true;
      confirm.textContent = 'Deleting…';
      setTimeout(function () {
        confirm.textContent = '✓ Deleted';
        confirm.classList.add('done');
        setTimeout(function () { busy = false; closeDialog(); }, 1000);
      }, 850);
    }
    
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeDialog();
    });

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { openDialog, closeDialog, overlayClick, checkPhrase, confirmDelete });
  }
}