Recipe
Progressive enhancement forms
Ship a zero-JS HTML form that works in every browser, then layer on client-side validation and optimistic UI when JavaScript loads.
Step 1 — Server-rendered form
<form action="/api/subscribe" method="POST">
<input
type="email"
name="email"
required
placeholder="you@example.com"
/>
<button type="submit">Subscribe</button>
</form>The required attribute gives native browser validation. The form POSTs even with JS disabled.
Step 2 — Sprinkle client validation
<script>
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const email = form.email.value;
if (!email.includes("@")) {
showError("Invalid email");
return;
}
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
});
if (res.ok) showSuccess();
});
</script>The script only runs when JS is available. The server endpoint handles both the enhanced fetch and the plain form POST.
Step 3 — Optimistic UI
// Show success immediately, roll back on failure
showSuccess();
try {
await fetch("/api/subscribe", {
method: "POST",
body: new FormData(form),
});
} catch {
showError("Failed — please try again");
}Optimistic updates make the UI feel instant. Always provide a rollback path when the server rejects the request.
Key principle
Start with working HTML. JavaScript is a progressive enhancement — never a requirement. Your form must function fully with<noscript>users, screen readers, and slow networks.