A modal asks the user for something and blocks the rest of the page. We will use the native <dialog> element so focus, Escape and the backdrop are handled by the browser (Dialog and Modal Basics).
<style>
body { font-family: system-ui, sans-serif; }
button { font: inherit; padding: 8px 14px; cursor: pointer; border-radius: 6px; border: 2px solid #1a5276; background: #fff; }
button.primary { background: #1a5276; color: #fff; }
button:focus-visible { outline: 3px solid #d94a00; outline-offset: 2px; }
dialog { border: 0; border-radius: 12px; padding: 20px; width: min(320px, 90vw); box-shadow: 0 10px 30px rgba(0,0,0,.3); }
dialog::backdrop { background: rgba(0, 0, 0, .5); }
dialog h2 { margin-top: 0; }
.actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
label { display: block; margin: 8px 0 4px; }
input { width: 100%; box-sizing: border-box; padding: 8px; font: inherit; }
</style>
<button id="open" class="primary">Subscribe</button>
<p id="result" role="status"></p>
<dialog id="dialog" aria-labelledby="title">
<form method="dialog" id="form">
<h2 id="title">Join the newsletter</h2>
<label for="email">Email</label>
<input id="email" name="email" type="email" required autocomplete="email">
<div class="actions">
<button value="cancel" formnovalidate>Cancel</button>
<button value="subscribe" class="primary">Subscribe</button>
</div>
</form>
</dialog>
<script>
const dialog = document.querySelector("#dialog");
const opener = document.querySelector("#open");
const result = document.querySelector("#result");
opener.addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
if (dialog.returnValue === "subscribe") {
const email = document.querySelector("#email").value;
result.textContent = `Thanks! We will write to ${email}.`;
} else {
result.textContent = "Cancelled.";
}
document.querySelector("#form").reset();
});
// clicking the dimmed backdrop closes the dialog
dialog.addEventListener("click", event => {
if (event.target === dialog) dialog.close("cancel");
});
</script>
What the browser does for you
showModal()puts the dialog above the page, makes the rest inert, moves focus inside and keeps Tab in the dialog.- Escape closes it.
- On close, focus returns to the button that opened it.
<form method="dialog">closes the dialog on submit, anddialog.returnValueholds the value of the pressed button. Therequiredemail is validated by the browser before the form closes.
What the script does
- Opens the dialog on click.
- Reads the return value in the
closeevent to decide what happened. - Resets the form so old text is not shown next time.
- Closes on a click on the backdrop (
event.target === dialog).
Try changing it
- Add a Delete confirmation dialog: “Delete this item?” with Cancel as the default (
autofocus). - Make the dialog open when the URL has
#subscribe. - Add an error message inside the dialog for invalid emails (Custom Validation).
- Style the opening animation using the
@starting-stylerule andtransition(check browser support).