/* 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'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
});
/* 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.