Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="cf-i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.3 2.6 17.6a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="cf-i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 7h15M9.5 7V4.8h5V7M6.5 7l.8 12.2a1.5 1.5 0 0 0 1.5 1.3h6.4a1.5 1.5 0 0 0 1.5-1.3L17.5 7M10.5 11v6M13.5 11v6"/></symbol>
  <symbol id="cf-i-commit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.5"/><path d="M3 12h5.5M15.5 12H21"/></symbol>
  <symbol id="cf-i-issue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="1.2"/></symbol>
  <symbol id="cf-i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15.5" r="4"/><path d="M10.9 12.6 19.5 4M16.2 7.3l2.6 2.6M13.9 9.6l2 2"/></symbol>
  <symbol id="cf-i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h4M13.5 3.5V8H18M13.5 3.5 18 8v2.5"/><path d="M19.2 13.3a1.6 1.6 0 0 1 2.3 2.3l-4.9 4.9-3 .7.7-3z"/></symbol>
  <symbol id="cf-i-exit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5h4A1.5 1.5 0 0 1 19.5 6v12a1.5 1.5 0 0 1-1.5 1.5h-4M10 16.5 5.5 12 10 7.5M5.5 12h9"/></symbol>
  <symbol id="cf-i-merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6.5" cy="5.5" r="2.2"/><circle cx="6.5" cy="18.5" r="2.2"/><circle cx="17.5" cy="12" r="2.2"/><path d="M6.5 7.7v8.6M6.5 7.7c0 3.2 2.6 4.3 8.8 4.3"/></symbol>
  <symbol id="cf-i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5zM13.5 3.5v5h5"/></symbol>
</svg>

<div><span class="lbl">Delete with typed confirmation</span>
<div class="btns"><button class="cf-btn is-danger-outline" type="button" data-open="cf-delete" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#cf-i-trash"/></svg>Delete repository</button></div></div>

<div><span class="lbl">Unsaved changes</span>
<div class="btns"><button class="cf-btn" type="button" data-open="cf-unsaved" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#cf-i-exit"/></svg>Leave editor</button></div></div>

<div><span class="lbl">Success message popup</span>
<div class="btns"><button class="cf-btn is-success" type="button" data-open="cf-merged" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#cf-i-merge"/></svg>Merge pull request</button></div></div>

<!-- Delete with typed confirmation: the button unlocks only when the name matches -->
<dialog class="cf" id="cf-delete" role="alertdialog" aria-labelledby="cf-delete-title" aria-describedby="cf-delete-desc">
  <form class="cf-form" method="dialog">
    <div class="cf-head">
      <span class="cf-icon is-danger" aria-hidden="true"><svg><use href="#cf-i-alert"/></svg></span>
      <div class="cf-titles">
        <h2 class="cf-title" id="cf-delete-title">Delete northwind/web-storefront?</h2>
        <p class="cf-text" id="cf-delete-desc">This permanently deletes the repository, its wiki and its release history. It cannot be undone.</p>
      </div>
    </div>
    <div class="cf-body">
      <ul class="cf-loss" aria-label="Also removed">
        <li><svg aria-hidden="true"><use href="#cf-i-commit"/></svg><span><b>1,284 commits</b> on 23 branches</span></li>
        <li><svg aria-hidden="true"><use href="#cf-i-issue"/></svg><span><b>36 open issues</b> and 9 pull requests</span></li>
        <li><svg aria-hidden="true"><use href="#cf-i-key"/></svg><span><b>4 deploy keys</b> and 2 webhooks</span></li>
      </ul>
      <label class="cf-label" for="cf-delete-input">Type <code>web-storefront</code> to confirm</label>
      <input class="cf-input" id="cf-delete-input" name="confirm" type="text" required pattern="web-storefront" data-cf-match="web-storefront"
        autocomplete="off" autocapitalize="off" spellcheck="false" autofocus>
    </div>
    <div class="cf-foot">
      <button class="cf-btn" value="cancel" formnovalidate>Cancel</button>
      <button class="cf-btn is-danger" value="delete" data-cf-submit disabled>Delete repository</button>
    </div>
  </form>
</dialog>

<!-- Unsaved changes: three ways out, the safe one focused first -->
<dialog class="cf is-wide" id="cf-unsaved" role="alertdialog" aria-labelledby="cf-unsaved-title" aria-describedby="cf-unsaved-desc">
  <form class="cf-form" method="dialog">
    <div class="cf-head">
      <span class="cf-icon" aria-hidden="true"><svg><use href="#cf-i-edit"/></svg></span>
      <div class="cf-titles">
        <h2 class="cf-title" id="cf-unsaved-title">Leave without saving?</h2>
        <p class="cf-text" id="cf-unsaved-desc"><code>docs/release-notes.md</code> has 3 unsaved changes. If you leave the editor now, they will be lost.</p>
      </div>
    </div>
    <div class="cf-body">
      <ul class="cf-changes" aria-label="Unsaved changes">
        <li><span class="cf-mark is-add" aria-hidden="true">+</span><span><span class="cf-sr">Added: </span>Heading “Dark mode tokens”</span><span class="cf-ln">L12</span></li>
        <li><span class="cf-mark is-edit" aria-hidden="true">~</span><span><span class="cf-sr">Changed: </span>Rewrote the migration steps</span><span class="cf-ln">L18–24</span></li>
        <li><span class="cf-mark is-del" aria-hidden="true">−</span><span><span class="cf-sr">Removed: </span>Link to issue #455</span><span class="cf-ln">L41</span></li>
      </ul>
    </div>
    <div class="cf-foot is-stack">
      <button class="cf-btn is-danger-outline" value="discard">Discard</button>
      <button class="cf-btn" value="keep" autofocus>Keep editing</button>
      <button class="cf-btn is-dark" value="save">Save and leave</button>
    </div>
  </form>
</dialog>

<!-- Success message popup -->
<dialog class="cf is-center" id="cf-merged" aria-labelledby="cf-merged-title" aria-describedby="cf-merged-desc">
  <form class="cf-form" method="dialog">
    <div class="cf-hero">
      <span class="cf-badge" aria-hidden="true">
        <svg class="cf-check" viewBox="0 0 64 64"><circle cx="32" cy="32" r="29" pathLength="1" stroke-dasharray="1" transform="rotate(-90 32 32)"/><path d="M20 33.5l8 8 16.5-17" pathLength="1" stroke-dasharray="1"/></svg>
      </span>
      <h2 class="cf-title" id="cf-merged-title">Pull request merged</h2>
      <p class="cf-text" id="cf-merged-desc">#482 Add dark mode tokens was merged into <code>main</code> by Ada Okafor.</p>
    </div>
    <ul class="cf-meta">
      <li><svg aria-hidden="true"><use href="#cf-i-commit"/></svg><span class="cf-sr">Commit </span><code>9f3c2e1</code></li>
      <li><svg aria-hidden="true"><use href="#cf-i-file"/></svg>14 files changed</li>
    </ul>
    <div class="cf-foot">
      <a class="cf-btn is-link" href="#">View commit</a>
      <button class="cf-btn is-dark" value="done" autofocus>Done</button>
    </div>
  </form>
</dialog>
CSS
/* Confirmation dialogs on the native <dialog> element. showModal() puts the dialog in the top layer,
   keeps focus inside it, closes it on Esc and makes the rest of the page inert. */
.cf-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.cf-btn{--cf-focus:#0f172a;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 16px;border:1px solid #cbd5e1;border-radius:8px;
  background:#fff;color:#0f172a;font-family:inherit;font-size:.875rem;font-weight:600;line-height:1.2;text-decoration:none;cursor:pointer;
  box-shadow:0 1px 2px rgba(15,23,42,.06);transition:background-color .15s,border-color .15s,color .15s,opacity .15s}
.cf-btn:hover{background:#f8fafc;border-color:#94a3b8}
.cf-btn:focus-visible{outline:2px solid var(--cf-focus);outline-offset:2px}
.cf-btn svg{width:17px;height:17px;flex:none}
.cf-btn.is-dark{background:#0f172a;border-color:#0f172a;color:#fff}
.cf-btn.is-dark:hover{background:#334155;border-color:#334155}
.cf-btn.is-danger{background:#dc2626;border-color:#dc2626;color:#fff;box-shadow:0 1px 2px rgba(220,38,38,.3)}
.cf-btn.is-danger:hover{background:#b91c1c;border-color:#b91c1c}
.cf-btn.is-danger-outline{border-color:#ef4444;color:#b91c1c}
.cf-btn.is-danger-outline:hover{background:#fef2f2;border-color:#dc2626}
.cf-btn.is-success{background:#15803d;border-color:#15803d;color:#fff;box-shadow:0 1px 2px rgba(21,128,61,.3)}
.cf-btn.is-success:hover{background:#166534;border-color:#166534}
.cf-btn.is-link{border-color:transparent;background:transparent;box-shadow:none;color:#334155;text-decoration:underline;text-decoration-color:#94a3b8;text-underline-offset:3px}
.cf-btn.is-link:hover{background:#f1f5f9;color:#0f172a;text-decoration-color:currentColor}
.cf-btn:disabled{opacity:.45;cursor:not-allowed}
.cf-btn.is-danger:disabled:hover{background:#dc2626;border-color:#dc2626}

.cf{--cf-ink:#0f172a;--cf-muted:#475569;--cf-line:#e2e8f0;--cf-danger:#dc2626;--cf-success:#15803d;--cf-w:460px;
  width:min(var(--cf-w),calc(100% - 32px));max-width:none;max-height:calc(100dvh - 48px);margin:auto;padding:0;border:1px solid var(--cf-line);border-radius:14px;
  background:#fff;color:var(--cf-ink);font-size:.925rem;line-height:1.55;overflow-y:auto;overscroll-behavior:contain;
  box-shadow:0 1px 2px rgba(15,23,42,.05),0 24px 56px -16px rgba(15,23,42,.4)}
.cf[open]{display:block;animation:cf-in .2s cubic-bezier(.2,.8,.2,1)}
.cf::backdrop{background:rgba(15,23,42,.6)}
.cf[open]::backdrop{animation:cf-fade .2s ease-out}
@keyframes cf-in{from{opacity:0;transform:scale(.96)}}
@keyframes cf-fade{from{opacity:0}}
html:has(.cf:modal){overflow:hidden} /* stop the page scrolling behind an open modal */

.cf-form{margin:0}
.cf-head{display:flex;align-items:flex-start;gap:16px;padding:24px 24px 0}
.cf-titles{flex:1;min-width:0}
.cf-title{margin:0;font-size:1.1rem;font-weight:700;letter-spacing:-.01em;line-height:1.35;overflow-wrap:anywhere}
.cf-text{margin:6px 0 0;color:var(--cf-muted);font-size:.9rem}
.cf-icon{flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:50%;background:#f1f5f9;color:#334155;box-shadow:0 0 0 6px #f8fafc}
.cf-icon.is-danger{background:#fee2e2;color:var(--cf-danger);box-shadow:0 0 0 6px #fef2f2}
.cf-icon svg{width:22px;height:22px}
.cf-body{padding:20px 24px 22px}
.cf-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;padding:16px 24px;border-top:1px solid var(--cf-line)}
.cf code{padding:1px 6px;border:1px solid var(--cf-line);border-radius:5px;background:#f1f5f9;color:var(--cf-ink);font:600 .85em/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}

/* Delete: what goes with it, and the typed confirmation */
.cf-loss{display:grid;gap:8px;margin:0 0 20px;padding:12px 14px;list-style:none;border:1px solid #fecaca;border-radius:10px;background:#fef2f2;color:#7f1d1d;font-size:.875rem}
.cf-loss li{display:flex;align-items:center;gap:10px}
.cf-loss svg{width:16px;height:16px;flex:none;color:var(--cf-danger)}
.cf-loss b{font-weight:600}
.cf-label{display:block;margin:0 0 8px;font-size:.875rem;font-weight:500}
.cf-input{display:block;width:100%;min-height:42px;margin:0;padding:9px 12px;border:1px solid #7c8798;border-radius:8px;background:#fff;color:var(--cf-ink);
  font:500 .9rem/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;transition:border-color .15s,box-shadow .15s}
.cf-input:focus-visible{outline:2px solid var(--cf-ink);outline-offset:1px}
.cf-input:valid{border-color:var(--cf-success);box-shadow:0 0 0 3px #dcfce7} /* the name matches */

/* Unsaved changes: the edits that would be lost */
.cf.is-wide{--cf-w:500px}
.cf-changes{margin:0;padding:0;list-style:none;border:1px solid var(--cf-line);border-radius:10px;font-size:.875rem}
.cf-changes li{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;padding:9px 12px}
.cf-changes li+li{border-top:1px solid var(--cf-line)}
.cf-mark{display:grid;place-items:center;width:20px;height:20px;border-radius:5px;font:700 .8rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.cf-mark.is-add{background:#dcfce7;color:#166534}
.cf-mark.is-edit{background:#e2e8f0;color:#334155}
.cf-mark.is-del{background:#fee2e2;color:#991b1b}
.cf-ln{color:#64748b;font:500 .78rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.cf-foot .cf-btn.is-danger-outline{margin-right:auto}

/* Success: a check that draws itself */
.cf.is-center{--cf-w:400px;text-align:center}
.cf-hero{padding:32px 28px 0}
.cf-badge{display:grid;place-items:center;width:88px;height:88px;margin:0 auto 18px;border-radius:50%;background:#f0fdf4}
.cf-check{display:block;width:64px;height:64px}
.cf-check circle{fill:#dcfce7;stroke:var(--cf-success);stroke-width:2.5}
.cf-check path{fill:none;stroke:var(--cf-success);stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round}
.cf[open] .cf-check circle{animation:cf-draw .45s cubic-bezier(.65,0,.35,1) both}
.cf[open] .cf-check path{animation:cf-draw .3s cubic-bezier(.65,0,.35,1) .3s both}
@keyframes cf-draw{from{stroke-dashoffset:1;opacity:0}8%{opacity:1}}
.cf-hero .cf-title{font-size:1.25rem}
.cf-meta{display:flex;margin:20px 24px 0;padding:0;list-style:none;border:1px solid var(--cf-line);border-radius:10px;background:#f8fafc;color:var(--cf-muted);font-size:.85rem}
.cf-meta li{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;padding:10px 8px}
.cf-meta li+li{border-left:1px solid var(--cf-line)}
.cf-meta svg{width:16px;height:16px;flex:none}
.cf-meta code{background:#fff}
.cf.is-center .cf-foot{display:grid;grid-template-columns:1fr 1fr;padding:22px 24px 24px;border-top:0}

@media (max-width:600px){
  .cf{max-height:calc(100dvh - 32px)}
  .cf-head{gap:14px;padding:20px 18px 0}
  .cf-body{padding:18px 18px 20px}
  .cf-foot{padding:14px 18px}
  .cf-foot .cf-btn{flex:1 1 auto}
  .cf-foot.is-stack{flex-direction:column-reverse}
  .cf-foot.is-stack .cf-btn{margin:0}
  .cf-meta{margin:18px 18px 0}
  .cf.is-center .cf-foot{padding:20px 18px}
}
@media (prefers-reduced-motion:reduce){
  .cf[open],.cf[open]::backdrop,.cf[open] .cf-check circle,.cf[open] .cf-check path{animation:none}
  .cf-btn,.cf-input{transition:none}
}
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('data-open'));
  if (!dialog) return;
  btn.addEventListener('click', function () {
    dialog.showModal();
    dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
  });
  // A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});

// Typed confirmation: the submit button stays disabled until the input matches data-cf-match exactly.
// required + pattern on the input block the form as well, so Enter cannot submit a wrong value.
document.querySelectorAll('[data-cf-match]').forEach(function (input) {
  var submit = input.form.querySelector('[data-cf-submit]');
  var dialog = input.closest('dialog');
  function sync() { submit.disabled = input.value !== input.getAttribute('data-cf-match'); }
  input.addEventListener('input', sync);
  input.addEventListener('keydown', function (e) { // Enter confirms (the form's first button is Cancel)
    if (e.key !== 'Enter') return;
    e.preventDefault();
    if (!submit.disabled) input.form.requestSubmit(submit);
  });
  dialog.addEventListener('close', function () { input.value = ''; sync(); }); // start empty next time
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="cf-i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.3 2.6 17.6a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="cf-i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 7h15M9.5 7V4.8h5V7M6.5 7l.8 12.2a1.5 1.5 0 0 0 1.5 1.3h6.4a1.5 1.5 0 0 0 1.5-1.3L17.5 7M10.5 11v6M13.5 11v6"/></symbol>
  <symbol id="cf-i-commit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.5"/><path d="M3 12h5.5M15.5 12H21"/></symbol>
  <symbol id="cf-i-issue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="1.2"/></symbol>
  <symbol id="cf-i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15.5" r="4"/><path d="M10.9 12.6 19.5 4M16.2 7.3l2.6 2.6M13.9 9.6l2 2"/></symbol>
  <symbol id="cf-i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h4M13.5 3.5V8H18M13.5 3.5 18 8v2.5"/><path d="M19.2 13.3a1.6 1.6 0 0 1 2.3 2.3l-4.9 4.9-3 .7.7-3z"/></symbol>
  <symbol id="cf-i-exit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5h4A1.5 1.5 0 0 1 19.5 6v12a1.5 1.5 0 0 1-1.5 1.5h-4M10 16.5 5.5 12 10 7.5M5.5 12h9"/></symbol>
  <symbol id="cf-i-merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6.5" cy="5.5" r="2.2"/><circle cx="6.5" cy="18.5" r="2.2"/><circle cx="17.5" cy="12" r="2.2"/><path d="M6.5 7.7v8.6M6.5 7.7c0 3.2 2.6 4.3 8.8 4.3"/></symbol>
  <symbol id="cf-i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5zM13.5 3.5v5h5"/></symbol>
</svg>

<div><span class="lbl">Delete with typed confirmation</span>
<div class="d-flex flex-wrap gap-2"><button class="btn btn-outline-danger cf-btn" type="button" data-bs-toggle="modal" data-bs-target="#cf-delete"><svg aria-hidden="true"><use href="#cf-i-trash"/></svg>Delete repository</button></div></div>

<div><span class="lbl">Unsaved changes</span>
<div class="d-flex flex-wrap gap-2"><button class="btn cf-btn" type="button" data-bs-toggle="modal" data-bs-target="#cf-unsaved"><svg aria-hidden="true"><use href="#cf-i-exit"/></svg>Leave editor</button></div></div>

<div><span class="lbl">Success message popup</span>
<div class="d-flex flex-wrap gap-2"><button class="btn btn-success cf-btn" type="button" data-bs-toggle="modal" data-bs-target="#cf-merged"><svg aria-hidden="true"><use href="#cf-i-merge"/></svg>Merge pull request</button></div></div>

<!-- Delete with typed confirmation: the button unlocks only when the name matches -->
<div class="modal fade cf" id="cf-delete" tabindex="-1" role="alertdialog" data-cf-alert aria-labelledby="cf-delete-title" aria-describedby="cf-delete-desc" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content">
      <div class="modal-header">
        <span class="cf-icon is-danger" aria-hidden="true"><svg><use href="#cf-i-alert"/></svg></span>
        <div class="cf-titles">
          <h2 class="modal-title" id="cf-delete-title">Delete northwind/web-storefront?</h2>
          <p class="cf-text" id="cf-delete-desc">This permanently deletes the repository, its wiki and its release history. It cannot be undone.</p>
        </div>
      </div>
      <div class="modal-body">
        <ul class="alert alert-danger list-unstyled cf-loss" aria-label="Also removed">
          <li><svg aria-hidden="true"><use href="#cf-i-commit"/></svg><span><b>1,284 commits</b> on 23 branches</span></li>
          <li><svg aria-hidden="true"><use href="#cf-i-issue"/></svg><span><b>36 open issues</b> and 9 pull requests</span></li>
          <li><svg aria-hidden="true"><use href="#cf-i-key"/></svg><span><b>4 deploy keys</b> and 2 webhooks</span></li>
        </ul>
        <label class="form-label" for="cf-delete-input">Type <code>web-storefront</code> to confirm</label>
        <input class="form-control cf-input" id="cf-delete-input" name="confirm" type="text" required pattern="web-storefront" data-cf-match="web-storefront" data-cf-focus
          autocomplete="off" autocapitalize="off" spellcheck="false">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn cf-btn" data-bs-dismiss="modal">Cancel</button>
        <button type="submit" class="btn btn-danger cf-btn" data-cf-submit disabled>Delete repository</button>
      </div>
    </form>
  </div>
</div>

<!-- Unsaved changes: three ways out, the safe one focused first -->
<div class="modal fade cf is-wide" id="cf-unsaved" tabindex="-1" role="alertdialog" data-cf-alert aria-labelledby="cf-unsaved-title" aria-describedby="cf-unsaved-desc" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <span class="cf-icon" aria-hidden="true"><svg><use href="#cf-i-edit"/></svg></span>
        <div class="cf-titles">
          <h2 class="modal-title" id="cf-unsaved-title">Leave without saving?</h2>
          <p class="cf-text" id="cf-unsaved-desc"><code>docs/release-notes.md</code> has 3 unsaved changes. If you leave the editor now, they will be lost.</p>
        </div>
      </div>
      <div class="modal-body">
        <ul class="list-group cf-changes" aria-label="Unsaved changes">
          <li class="list-group-item"><span class="badge cf-mark is-add" aria-hidden="true">+</span><span><span class="visually-hidden">Added: </span>Heading “Dark mode tokens”</span><span class="cf-ln">L12</span></li>
          <li class="list-group-item"><span class="badge cf-mark is-edit" aria-hidden="true">~</span><span><span class="visually-hidden">Changed: </span>Rewrote the migration steps</span><span class="cf-ln">L18–24</span></li>
          <li class="list-group-item"><span class="badge cf-mark is-del" aria-hidden="true">−</span><span><span class="visually-hidden">Removed: </span>Link to issue #455</span><span class="cf-ln">L41</span></li>
        </ul>
      </div>
      <div class="modal-footer is-stack">
        <button type="button" class="btn btn-outline-danger cf-btn me-auto" data-bs-dismiss="modal">Discard</button>
        <button type="button" class="btn cf-btn" data-bs-dismiss="modal" data-cf-focus>Keep editing</button>
        <button type="button" class="btn btn-dark cf-btn" data-bs-dismiss="modal">Save and leave</button>
      </div>
    </div>
  </div>
</div>

<!-- Success message popup -->
<div class="modal fade cf is-center" id="cf-merged" tabindex="-1" aria-labelledby="cf-merged-title" aria-describedby="cf-merged-desc" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content text-center">
      <div class="cf-hero">
        <span class="cf-badge" aria-hidden="true">
          <svg class="cf-check" viewBox="0 0 64 64"><circle cx="32" cy="32" r="29" pathLength="1" stroke-dasharray="1" transform="rotate(-90 32 32)"/><path d="M20 33.5l8 8 16.5-17" pathLength="1" stroke-dasharray="1"/></svg>
        </span>
        <h2 class="modal-title" id="cf-merged-title">Pull request merged</h2>
        <p class="cf-text" id="cf-merged-desc">#482 Add dark mode tokens was merged into <code>main</code> by Ada Okafor.</p>
      </div>
      <ul class="list-group list-group-horizontal cf-meta">
        <li class="list-group-item flex-fill"><svg aria-hidden="true"><use href="#cf-i-commit"/></svg><span class="visually-hidden">Commit </span><code>9f3c2e1</code></li>
        <li class="list-group-item flex-fill"><svg aria-hidden="true"><use href="#cf-i-file"/></svg>14 files changed</li>
      </ul>
      <div class="modal-footer">
        <a class="btn btn-link cf-btn" href="#">View commit</a>
        <button type="button" class="btn btn-dark cf-btn" data-bs-dismiss="modal" data-cf-focus>Done</button>
      </div>
    </div>
  </div>
</div>
CSS
/* Bootstrap's Modal component, restyled through its --bs-modal-*, --bs-btn-* and --bs-list-group-* variables. */
.cf-btn{--bs-btn-padding-x:16px;--bs-btn-padding-y:8px;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;--bs-btn-border-radius:8px;
  --bs-btn-bg:#fff;--bs-btn-color:#0f172a;--bs-btn-border-color:#cbd5e1;--bs-btn-hover-bg:#f8fafc;--bs-btn-hover-color:#0f172a;--bs-btn-hover-border-color:#94a3b8;
  --bs-btn-active-bg:#f1f5f9;--bs-btn-active-color:#0f172a;--bs-btn-active-border-color:#94a3b8;
  --bs-btn-focus-box-shadow:0 1px 2px rgba(15,23,42,.06);display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;box-shadow:var(--bs-btn-focus-box-shadow)}
.cf-btn.btn-dark{--bs-btn-bg:#0f172a;--bs-btn-color:#fff;--bs-btn-border-color:#0f172a;--bs-btn-hover-bg:#334155;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#334155;
  --bs-btn-active-bg:#1e293b;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#1e293b}
.cf-btn.btn-danger{--bs-btn-bg:#dc2626;--bs-btn-color:#fff;--bs-btn-border-color:#dc2626;--bs-btn-hover-bg:#b91c1c;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#b91c1c;
  --bs-btn-active-bg:#991b1b;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#991b1b;
  --bs-btn-disabled-bg:#dc2626;--bs-btn-disabled-color:#fff;--bs-btn-disabled-border-color:#dc2626;--bs-btn-disabled-opacity:.45;--bs-btn-focus-box-shadow:0 1px 2px rgba(220,38,38,.3)}
.cf-btn.btn-outline-danger{--bs-btn-color:#b91c1c;--bs-btn-border-color:#ef4444;--bs-btn-hover-bg:#fef2f2;--bs-btn-hover-color:#b91c1c;--bs-btn-hover-border-color:#dc2626;
  --bs-btn-active-bg:#fee2e2;--bs-btn-active-color:#991b1b;--bs-btn-active-border-color:#dc2626}
.cf-btn.btn-success{--bs-btn-bg:#15803d;--bs-btn-color:#fff;--bs-btn-border-color:#15803d;--bs-btn-hover-bg:#166534;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#166534;
  --bs-btn-active-bg:#14532d;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#14532d;--bs-btn-focus-box-shadow:0 1px 2px rgba(21,128,61,.3)}
.cf-btn.btn-link{--bs-btn-color:#334155;--bs-btn-border-color:transparent;--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-color:#0f172a;--bs-btn-hover-border-color:transparent;
  --bs-btn-active-bg:#e2e8f0;--bs-btn-active-color:#0f172a;--bs-btn-focus-box-shadow:none;background:transparent;text-decoration:underline;text-decoration-color:#94a3b8;text-underline-offset:3px}
.cf-btn.btn-link:hover{background:#f1f5f9;text-decoration-color:currentColor}
.cf-btn:focus-visible{outline:2px solid #0f172a;outline-offset:2px}
.cf-btn:focus-visible:not(:hover){border-color:var(--bs-btn-border-color);background-color:var(--bs-btn-bg);color:var(--bs-btn-color)} /* Bootstrap paints focus like hover */
.cf-btn svg{width:17px;height:17px;flex:none}

.cf{--bs-modal-width:460px;--bs-modal-margin:1rem;--bs-modal-color:#0f172a;--bs-modal-bg:#fff;--bs-modal-border-color:#e2e8f0;--bs-modal-border-radius:14px;--bs-modal-inner-border-radius:13px;
  --bs-modal-header-padding:24px 24px 0;--bs-modal-header-border-width:0;--bs-modal-title-line-height:1.35;--bs-modal-footer-border-color:#e2e8f0;
  font-size:.925rem;line-height:1.55}
@media (min-width:576px){.cf{--bs-modal-margin:1.75rem}}
.cf.is-wide{--bs-modal-width:500px}
.cf .modal-content{box-shadow:0 1px 2px rgba(15,23,42,.05),0 24px 56px -16px rgba(15,23,42,.4)}
.cf .modal-header{align-items:flex-start;gap:16px}
.cf-titles{flex:1;min-width:0}
.cf .modal-title{font-size:1.1rem;font-weight:700;letter-spacing:-.01em;overflow-wrap:anywhere}
.cf-text{margin:6px 0 0;color:#475569;font-size:.9rem}
.cf .modal-body{padding:20px 24px 22px}
.cf .modal-footer{gap:10px;padding:16px 24px}
.cf .modal-footer>*{margin:0}
.cf-icon{flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:50%;background:#f1f5f9;color:#334155;box-shadow:0 0 0 6px #f8fafc}
.cf-icon.is-danger{background:#fee2e2;color:#dc2626;box-shadow:0 0 0 6px #fef2f2}
.cf-icon svg{width:22px;height:22px}
.cf code{padding:1px 6px;border:1px solid #e2e8f0;border-radius:5px;background:#f1f5f9;color:#0f172a;font:600 .85em/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}
/* Same entrance as the plain version: a short scale instead of Bootstrap's 50px drop */
.cf.fade .modal-dialog{transform:scale(.96);transition:transform .2s cubic-bezier(.2,.8,.2,1)}
.cf.show .modal-dialog{transform:none}
.modal-backdrop{--bs-backdrop-bg:#0f172a;--bs-backdrop-opacity:.6}

/* Delete: what goes with it, and the typed confirmation */
.cf-loss{--bs-alert-bg:#fef2f2;--bs-alert-color:#7f1d1d;--bs-alert-border-color:#fecaca;--bs-alert-padding-x:14px;--bs-alert-padding-y:12px;--bs-alert-margin-bottom:20px;--bs-alert-border-radius:10px;
  display:grid;gap:8px;font-size:.875rem}
.cf-loss li{display:flex;align-items:center;gap:10px}
.cf-loss svg{width:16px;height:16px;flex:none;color:#dc2626}
.cf-loss b{font-weight:600}
.cf .form-label{margin-bottom:8px;font-size:.875rem;font-weight:500}
.cf-input{min-height:42px;padding:9px 12px;border-color:#7c8798;border-radius:8px;color:#0f172a;font:500 .9rem/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.cf-input:focus{border-color:#7c8798;box-shadow:none}
.cf-input:focus-visible{outline:2px solid #0f172a;outline-offset:1px}
.cf-input:valid{border-color:#15803d;box-shadow:0 0 0 3px #dcfce7} /* the name matches */

/* Unsaved changes: the edits that would be lost */
.cf-changes{--bs-list-group-border-color:#e2e8f0;--bs-list-group-border-radius:10px;--bs-list-group-item-padding-x:12px;--bs-list-group-item-padding-y:9px;--bs-list-group-color:#0f172a;font-size:.875rem}
.cf-changes .list-group-item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px}
.cf-mark{display:grid;place-items:center;width:20px;height:20px;padding:0;border-radius:5px;font:700 .8rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.cf-mark.is-add{background:#dcfce7;color:#166534}
.cf-mark.is-edit{background:#e2e8f0;color:#334155}
.cf-mark.is-del{background:#fee2e2;color:#991b1b}
.cf-ln{color:#64748b;font:500 .78rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}

/* Success: a check that draws itself (it restarts each time the modal is shown) */
.cf.is-center{--bs-modal-width:400px}
.cf-hero{padding:32px 28px 0}
.cf-badge{display:grid;place-items:center;width:88px;height:88px;margin:0 auto 18px;border-radius:50%;background:#f0fdf4}
.cf-check{display:block;width:64px;height:64px}
.cf-check circle{fill:#dcfce7;stroke:#15803d;stroke-width:2.5}
.cf-check path{fill:none;stroke:#15803d;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round}
.cf .cf-check circle{animation:cf-draw .45s cubic-bezier(.65,0,.35,1) both}
.cf .cf-check path{animation:cf-draw .3s cubic-bezier(.65,0,.35,1) .3s both}
@keyframes cf-draw{from{stroke-dashoffset:1;opacity:0}8%{opacity:1}}
.cf-hero .modal-title{font-size:1.25rem}
.cf-meta{--bs-list-group-bg:#f8fafc;--bs-list-group-color:#475569;--bs-list-group-border-color:#e2e8f0;--bs-list-group-border-radius:10px;--bs-list-group-item-padding-x:8px;--bs-list-group-item-padding-y:10px;
  margin:20px 24px 0;font-size:.85rem}
.cf-meta .list-group-item{display:flex;align-items:center;justify-content:center;gap:8px}
.cf-meta svg{width:16px;height:16px;flex:none}
.cf-meta code{background:#fff}
.cf.is-center .modal-footer{display:grid;grid-template-columns:1fr 1fr;padding:22px 24px 24px;border-top:0}

@media (max-width:600px){
  .cf{--bs-modal-header-padding:20px 18px 0}
  .cf .modal-header{gap:14px}
  .cf .modal-body{padding:18px 18px 20px}
  .cf .modal-footer{padding:14px 18px}
  .cf .modal-footer .cf-btn{flex:1 1 auto}
  .cf .modal-footer.is-stack{flex-direction:column-reverse;align-items:stretch}
  .cf .modal-footer.is-stack .cf-btn{margin:0!important}
  .cf-meta{margin:18px 18px 0}
  .cf.is-center .modal-footer{padding:20px 18px}
}
@media (prefers-reduced-motion:reduce){
  .cf.fade .modal-dialog,.cf-btn,.cf .cf-check circle,.cf .cf-check path{transition:none;animation:none}
}
JavaScript
// Bootstrap sets role="dialog" on every modal it shows, so give the confirmations their alertdialog
// role back, then move focus to the control marked data-cf-focus (Bootstrap focuses the modal box).
document.querySelectorAll('.cf.modal').forEach(function (modal) {
  modal.addEventListener('shown.bs.modal', function () {
    if (modal.hasAttribute('data-cf-alert')) modal.setAttribute('role', 'alertdialog');
    var first = modal.querySelector('[data-cf-focus]');
    if (first) first.focus();
  });
});

// Typed confirmation: the submit button stays disabled until the input matches data-cf-match exactly.
// required + pattern on the input stop the form submitting a wrong value in any case.
document.querySelectorAll('[data-cf-match]').forEach(function (input) {
  var form = input.form;
  var submit = form.querySelector('[data-cf-submit]');
  var modal = input.closest('.modal');
  function sync() { submit.disabled = input.value !== input.getAttribute('data-cf-match'); }
  input.addEventListener('input', sync);
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // send your delete request here
    bootstrap.Modal.getInstance(modal).hide();
  });
  modal.addEventListener('hidden.bs.modal', function () { input.value = ''; sync(); }); // start empty next time
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="cf-i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.3 2.6 17.6a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="cf-i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 7h15M9.5 7V4.8h5V7M6.5 7l.8 12.2a1.5 1.5 0 0 0 1.5 1.3h6.4a1.5 1.5 0 0 0 1.5-1.3L17.5 7M10.5 11v6M13.5 11v6"/></symbol>
  <symbol id="cf-i-commit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.5"/><path d="M3 12h5.5M15.5 12H21"/></symbol>
  <symbol id="cf-i-issue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="1.2"/></symbol>
  <symbol id="cf-i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15.5" r="4"/><path d="M10.9 12.6 19.5 4M16.2 7.3l2.6 2.6M13.9 9.6l2 2"/></symbol>
  <symbol id="cf-i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h4M13.5 3.5V8H18M13.5 3.5 18 8v2.5"/><path d="M19.2 13.3a1.6 1.6 0 0 1 2.3 2.3l-4.9 4.9-3 .7.7-3z"/></symbol>
  <symbol id="cf-i-exit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5h4A1.5 1.5 0 0 1 19.5 6v12a1.5 1.5 0 0 1-1.5 1.5h-4M10 16.5 5.5 12 10 7.5M5.5 12h9"/></symbol>
  <symbol id="cf-i-merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6.5" cy="5.5" r="2.2"/><circle cx="6.5" cy="18.5" r="2.2"/><circle cx="17.5" cy="12" r="2.2"/><path d="M6.5 7.7v8.6M6.5 7.7c0 3.2 2.6 4.3 8.8 4.3"/></symbol>
  <symbol id="cf-i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5zM13.5 3.5v5h5"/></symbol>
</svg>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Delete with typed confirmation</span>
<div class="flex flex-wrap gap-2.5"><button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#ef4444] bg-white text-[#b91c1c] hover:border-[#dc2626] hover:bg-[#fef2f2]" type="button" data-open="cf-delete" aria-haspopup="dialog"><svg class="size-[17px] flex-none" aria-hidden="true"><use href="#cf-i-trash"/></svg>Delete repository</button></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Unsaved changes</span>
<div class="flex flex-wrap gap-2.5"><button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f8fafc]" type="button" data-open="cf-unsaved" aria-haspopup="dialog"><svg class="size-[17px] flex-none" aria-hidden="true"><use href="#cf-i-exit"/></svg>Leave editor</button></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Success message popup</span>
<div class="flex flex-wrap gap-2.5"><button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline border-[#15803d] bg-[#15803d] text-white shadow-[0_1px_2px_rgba(21,128,61,.3)] hover:border-[#166534] hover:bg-[#166534]" type="button" data-open="cf-merged" aria-haspopup="dialog"><svg class="size-[17px] flex-none" aria-hidden="true"><use href="#cf-i-merge"/></svg>Merge pull request</button></div></div>

<!-- Delete with typed confirmation: the button unlocks only when the name matches -->
<dialog class="m-auto max-h-[calc(100dvh-48px)] w-[min(460px,calc(100%-32px))] max-w-none overflow-y-auto overscroll-contain rounded-[14px] border border-[#e2e8f0] bg-white p-0 text-[.925rem] leading-[1.55] text-[#0f172a] shadow-[0_1px_2px_rgba(15,23,42,.05),0_24px_56px_-16px_rgba(15,23,42,.4)] backdrop:bg-[rgba(15,23,42,.6)] motion-safe:open:animate-[cf-in_.2s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[cf-fade_.2s_ease-out] max-[600px]:max-h-[calc(100dvh-32px)]" id="cf-delete" role="alertdialog" aria-labelledby="cf-delete-title" aria-describedby="cf-delete-desc">
  <form class="m-0" method="dialog">
    <div class="flex items-start gap-4 px-6 pt-6 max-[600px]:gap-3.5 max-[600px]:px-[18px] max-[600px]:pt-5">
      <span class="grid size-11 flex-none place-items-center rounded-full bg-[#fee2e2] text-[#dc2626] shadow-[0_0_0_6px_#fef2f2]" aria-hidden="true"><svg class="size-[22px]"><use href="#cf-i-alert"/></svg></span>
      <div class="min-w-0 flex-1">
        <h2 class="m-0 text-[1.1rem] leading-[1.35] font-bold tracking-[-.01em] wrap-anywhere" id="cf-delete-title">Delete northwind/web-storefront?</h2>
        <p class="mt-1.5 mb-0 text-[.9rem] text-[#475569]" id="cf-delete-desc">This permanently deletes the repository, its wiki and its release history. It cannot be undone.</p>
      </div>
    </div>
    <div class="px-6 pt-5 pb-[22px] max-[600px]:px-[18px] max-[600px]:pt-[18px] max-[600px]:pb-5">
      <ul class="mx-0 mt-0 mb-5 grid list-none gap-2 rounded-[10px] border border-[#fecaca] bg-[#fef2f2] px-3.5 py-3 text-[.875rem] text-[#7f1d1d]" aria-label="Also removed">
        <li class="flex items-center gap-2.5"><svg class="size-4 flex-none text-[#dc2626]" aria-hidden="true"><use href="#cf-i-commit"/></svg><span><b class="font-semibold">1,284 commits</b> on 23 branches</span></li>
        <li class="flex items-center gap-2.5"><svg class="size-4 flex-none text-[#dc2626]" aria-hidden="true"><use href="#cf-i-issue"/></svg><span><b class="font-semibold">36 open issues</b> and 9 pull requests</span></li>
        <li class="flex items-center gap-2.5"><svg class="size-4 flex-none text-[#dc2626]" aria-hidden="true"><use href="#cf-i-key"/></svg><span><b class="font-semibold">4 deploy keys</b> and 2 webhooks</span></li>
      </ul>
      <label class="mb-2 block text-[.875rem] font-medium" for="cf-delete-input">Type <code class="rounded-[5px] border border-[#e2e8f0] px-1.5 py-px font-mono text-[.85em] leading-[1.4] font-semibold text-[#0f172a] wrap-anywhere bg-[#f1f5f9]">web-storefront</code> to confirm</label>
      <input class="block min-h-[42px] w-full rounded-lg border border-[#7c8798] bg-white px-3 py-[9px] font-mono text-[.9rem] leading-[1.4] font-medium text-[#0f172a] transition-[border-color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#0f172a] valid:border-[#15803d] valid:shadow-[0_0_0_3px_#dcfce7] motion-reduce:transition-none" id="cf-delete-input" name="confirm" type="text" required pattern="web-storefront" data-cf-match="web-storefront"
        autocomplete="off" autocapitalize="off" spellcheck="false" autofocus>
    </div>
    <div class="flex flex-wrap justify-end gap-2.5 border-t border-[#e2e8f0] px-6 py-4 max-[600px]:px-[18px] max-[600px]:py-3.5">
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f8fafc] max-[600px]:flex-auto" value="cancel" formnovalidate>Cancel</button>
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline border-[#dc2626] bg-[#dc2626] text-white shadow-[0_1px_2px_rgba(220,38,38,.3)] enabled:hover:border-[#b91c1c] enabled:hover:bg-[#b91c1c] max-[600px]:flex-auto" value="delete" data-cf-submit disabled>Delete repository</button>
    </div>
  </form>
</dialog>

<!-- Unsaved changes: three ways out, the safe one focused first -->
<dialog class="m-auto max-h-[calc(100dvh-48px)] w-[min(500px,calc(100%-32px))] max-w-none overflow-y-auto overscroll-contain rounded-[14px] border border-[#e2e8f0] bg-white p-0 text-[.925rem] leading-[1.55] text-[#0f172a] shadow-[0_1px_2px_rgba(15,23,42,.05),0_24px_56px_-16px_rgba(15,23,42,.4)] backdrop:bg-[rgba(15,23,42,.6)] motion-safe:open:animate-[cf-in_.2s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[cf-fade_.2s_ease-out] max-[600px]:max-h-[calc(100dvh-32px)]" id="cf-unsaved" role="alertdialog" aria-labelledby="cf-unsaved-title" aria-describedby="cf-unsaved-desc">
  <form class="m-0" method="dialog">
    <div class="flex items-start gap-4 px-6 pt-6 max-[600px]:gap-3.5 max-[600px]:px-[18px] max-[600px]:pt-5">
      <span class="grid size-11 flex-none place-items-center rounded-full bg-[#f1f5f9] text-[#334155] shadow-[0_0_0_6px_#f8fafc]" aria-hidden="true"><svg class="size-[22px]"><use href="#cf-i-edit"/></svg></span>
      <div class="min-w-0 flex-1">
        <h2 class="m-0 text-[1.1rem] leading-[1.35] font-bold tracking-[-.01em] wrap-anywhere" id="cf-unsaved-title">Leave without saving?</h2>
        <p class="mt-1.5 mb-0 text-[.9rem] text-[#475569]" id="cf-unsaved-desc"><code class="rounded-[5px] border border-[#e2e8f0] px-1.5 py-px font-mono text-[.85em] leading-[1.4] font-semibold text-[#0f172a] wrap-anywhere bg-[#f1f5f9]">docs/release-notes.md</code> has 3 unsaved changes. If you leave the editor now, they will be lost.</p>
      </div>
    </div>
    <div class="px-6 pt-5 pb-[22px] max-[600px]:px-[18px] max-[600px]:pt-[18px] max-[600px]:pb-5">
      <ul class="m-0 list-none rounded-[10px] border border-[#e2e8f0] p-0 text-[.875rem]" aria-label="Unsaved changes">
        <li class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2.5 border-[#e2e8f0] px-3 py-[9px] not-first:border-t"><span class="grid size-5 place-items-center rounded-[5px] font-mono text-[.8rem] leading-none font-bold bg-[#dcfce7] text-[#166534]" aria-hidden="true">+</span><span><span class="sr-only">Added: </span>Heading “Dark mode tokens”</span><span class="font-mono text-[.78rem] leading-none font-medium text-[#64748b]">L12</span></li>
        <li class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2.5 border-[#e2e8f0] px-3 py-[9px] not-first:border-t"><span class="grid size-5 place-items-center rounded-[5px] font-mono text-[.8rem] leading-none font-bold bg-[#e2e8f0] text-[#334155]" aria-hidden="true">~</span><span><span class="sr-only">Changed: </span>Rewrote the migration steps</span><span class="font-mono text-[.78rem] leading-none font-medium text-[#64748b]">L18–24</span></li>
        <li class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2.5 border-[#e2e8f0] px-3 py-[9px] not-first:border-t"><span class="grid size-5 place-items-center rounded-[5px] font-mono text-[.8rem] leading-none font-bold bg-[#fee2e2] text-[#991b1b]" aria-hidden="true">−</span><span><span class="sr-only">Removed: </span>Link to issue #455</span><span class="font-mono text-[.78rem] leading-none font-medium text-[#64748b]">L41</span></li>
      </ul>
    </div>
    <div class="flex flex-wrap justify-end gap-2.5 border-t border-[#e2e8f0] px-6 py-4 max-[600px]:px-[18px] max-[600px]:py-3.5 max-[600px]:flex-col-reverse">
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#ef4444] bg-white text-[#b91c1c] hover:border-[#dc2626] hover:bg-[#fef2f2] max-[600px]:flex-auto mr-auto max-[600px]:mr-0" value="discard">Discard</button>
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f8fafc] max-[600px]:flex-auto" value="keep" autofocus>Keep editing</button>
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#0f172a] bg-[#0f172a] text-white hover:border-[#334155] hover:bg-[#334155] max-[600px]:flex-auto" value="save">Save and leave</button>
    </div>
  </form>
</dialog>

<!-- Success message popup -->
<dialog class="m-auto max-h-[calc(100dvh-48px)] w-[min(400px,calc(100%-32px))] max-w-none overflow-y-auto overscroll-contain rounded-[14px] border border-[#e2e8f0] bg-white p-0 text-[.925rem] leading-[1.55] text-[#0f172a] shadow-[0_1px_2px_rgba(15,23,42,.05),0_24px_56px_-16px_rgba(15,23,42,.4)] backdrop:bg-[rgba(15,23,42,.6)] motion-safe:open:animate-[cf-in_.2s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[cf-fade_.2s_ease-out] max-[600px]:max-h-[calc(100dvh-32px)] text-center" id="cf-merged" aria-labelledby="cf-merged-title" aria-describedby="cf-merged-desc">
  <form class="m-0" method="dialog">
    <div class="px-7 pt-8">
      <span class="mx-auto mb-[18px] grid size-[88px] place-items-center rounded-full bg-[#f0fdf4]" aria-hidden="true">
        <svg class="block size-16" viewBox="0 0 64 64"><circle class="fill-[#dcfce7] stroke-[#15803d] [stroke-width:2.5] motion-safe:animate-[cf-draw_.45s_cubic-bezier(.65,0,.35,1)_both]" cx="32" cy="32" r="29" pathLength="1" stroke-dasharray="1" transform="rotate(-90 32 32)"/><path class="fill-none stroke-[#15803d] [stroke-width:3.5] [stroke-linecap:round] [stroke-linejoin:round] motion-safe:animate-[cf-draw_.3s_cubic-bezier(.65,0,.35,1)_.3s_both]" d="M20 33.5l8 8 16.5-17" pathLength="1" stroke-dasharray="1"/></svg>
      </span>
      <h2 class="m-0 text-[1.25rem] leading-[1.35] font-bold tracking-[-.01em]" id="cf-merged-title">Pull request merged</h2>
      <p class="mt-1.5 mb-0 text-[.9rem] text-[#475569]" id="cf-merged-desc">#482 Add dark mode tokens was merged into <code class="rounded-[5px] border border-[#e2e8f0] px-1.5 py-px font-mono text-[.85em] leading-[1.4] font-semibold text-[#0f172a] wrap-anywhere bg-[#f1f5f9]">main</code> by Ada Okafor.</p>
    </div>
    <ul class="mx-6 mt-5 mb-0 flex list-none rounded-[10px] border border-[#e2e8f0] bg-[#f8fafc] p-0 text-[.85rem] text-[#475569] max-[600px]:mx-[18px] max-[600px]:mt-[18px]">
      <li class="flex flex-1 items-center justify-center gap-2 border-[#e2e8f0] px-2 py-2.5 not-first:border-l"><svg class="size-4 flex-none" aria-hidden="true"><use href="#cf-i-commit"/></svg><span class="sr-only">Commit </span><code class="rounded-[5px] border border-[#e2e8f0] px-1.5 py-px font-mono text-[.85em] leading-[1.4] font-semibold text-[#0f172a] wrap-anywhere bg-white">9f3c2e1</code></li>
      <li class="flex flex-1 items-center justify-center gap-2 border-[#e2e8f0] px-2 py-2.5 not-first:border-l"><svg class="size-4 flex-none" aria-hidden="true"><use href="#cf-i-file"/></svg>14 files changed</li>
    </ul>
    <div class="grid grid-cols-2 gap-2.5 px-6 pt-[22px] pb-6 max-[600px]:px-[18px] max-[600px]:py-5">
      <a class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 border-transparent bg-transparent text-[#334155] underline decoration-[#94a3b8] underline-offset-3 hover:bg-[#f1f5f9] hover:text-[#0f172a] hover:decoration-current" href="#">View commit</a>
      <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-lg border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-45 no-underline shadow-[0_1px_2px_rgba(15,23,42,.06)] border-[#0f172a] bg-[#0f172a] text-white hover:border-[#334155] hover:bg-[#334155]" value="done" autofocus>Done</button>
    </div>
  </form>
</dialog>
CSS
/* Entrance keyframes for the dialog and its backdrop, and the self-drawing check (used through motion-safe:…animate-[…]) */
@keyframes cf-in { from { opacity: 0; transform: scale(.96); } }
@keyframes cf-fade { from { opacity: 0; } }
@keyframes cf-draw { from { stroke-dashoffset: 1; opacity: 0; } 8% { opacity: 1; } }
/* Stop the page scrolling behind an open modal */
html:has(dialog:modal) { overflow: hidden; }
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('data-open'));
  if (!dialog) return;
  btn.addEventListener('click', function () {
    dialog.showModal();
    dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
  });
  // A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});

// Typed confirmation: the submit button stays disabled until the input matches data-cf-match exactly.
// required + pattern on the input block the form as well, so Enter cannot submit a wrong value.
document.querySelectorAll('[data-cf-match]').forEach(function (input) {
  var submit = input.form.querySelector('[data-cf-submit]');
  var dialog = input.closest('dialog');
  function sync() { submit.disabled = input.value !== input.getAttribute('data-cf-match'); }
  input.addEventListener('input', sync);
  input.addEventListener('keydown', function (e) { // Enter confirms (the form's first button is Cancel)
    if (e.key !== 'Enter') return;
    e.preventDefault();
    if (!submit.disabled) input.form.requestSubmit(submit);
  });
  dialog.addEventListener('close', function () { input.value = ''; sync(); }); // start empty next time
});

About this set

Three confirmation modals from a code-hosting app, for the moments when an action needs a second look or a clear result. Delete with typed confirmation asks the reader to type the repository name before the red Delete repository button unlocks, and lists what goes with it: commits, open issues, deploy keys and webhooks. Unsaved changes is a leave-the-editor prompt with three ways out, Discard, Keep editing and Save and leave, plus the three pending edits so the reader knows what is at stake. The success message popup confirms a merged pull request with a green check that draws itself, the commit hash and the number of files changed. Use the typed delete for anything that cannot be undone, the three-action prompt when leaving would lose work, and the success popup when a result deserves more than a toast. The two confirmations are alert dialogs with aria-describedby, so their consequence text is read out with the title. A few lines of JavaScript compare the typed name and enable the button, while required and pattern on the input also stop a wrong value from submitting. The Bootstrap version uses the Modal plugin and focuses the input on shown.bs.modal.

What’s included

  • Delete button unlocks only when the typed name matches
  • Three-action unsaved-changes prompt with the safe action focused first
  • Success popup with a self-drawing check mark
  • role="alertdialog" and aria-describedby on the confirmations
  • required and pattern block a wrong value even without the script

Accessibility

The delete and unsaved-changes dialogs are alert dialogs labelled by their heading and described by their consequence text; the success popup is a labelled dialog. Focus starts on the confirmation input, on Keep editing, or on Done, Esc closes each one and focus returns to the button that opened it. The input has a visible label, the change list includes visually hidden Added, Changed and Removed words, every button shows a focus outline, and the pop-in and the drawing check are static under prefers-reduced-motion.

Customise it

In plain CSS change the --cf-* custom properties on .cf (ink, muted, line, danger, success and --cf-w for the width) and the .cf-btn variants. In Bootstrap the dialogs use --bs-modal-* variables and the buttons --bs-btn-*; in Tailwind edit the red, green and slate hex utilities on the dialogs and buttons.