Double-submits are as old as forms on the web: the user clicks “Submit”, nothing appears to happen fast enough, they click again, and now you’ve charged the card twice. The classic fix — disable the button on submit — is still right, but the 2008 implementations had landmines worth knowing about.
The old way, and why it broke
The era-typical script sniffed document.all to detect Internet Explorer, then looped through form elements disabling every submit and reset button from an onSubmit attribute. document.all is long dead, and the inline-handler style didn’t compose. Worse, it had a subtle bug baked in: a disabled submit button is not included in the form submission. Some browsers would therefore submit without the button’s name/value pair — breaking any server code that checked if (isset($_POST['save'])).
The modern version
Attach a submit listener and disable after the browser has captured the submission:
document.querySelector('form').addEventListener('submit', (e) => {
const button = e.target.querySelector('button[type="submit"]');
if (button.disabled) {
e.preventDefault(); // guard against re-entry
return;
}
button.disabled = true;
button.textContent = 'Submitting…';
});
If you ever trigger submission programmatically, use form.requestSubmit() rather than form.submit() — the latter skips HTML validation and submit event handlers entirely, which defeats the guard.
Modern CSS makes the disabled state self-explanatory:
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
The real fix is server-side
Client-side disabling is a courtesy for slow connections, not a guarantee — a reload, a back-button resubmit, or a retrying HTTP client bypasses it completely. For anything with side effects, make the operation idempotent on the server:
- Issue a one-time token when rendering the form; reject any submission whose token was already consumed.
- Or include a client-generated request ID and deduplicate on it.
- Post-Redirect-Get (PRG) pattern after successful submission kills the back-button resubmit case.
Do the button-disable for UX, the idempotency for correctness.