Provide Clear Form Submission Feedback
Impact: HIGH (Prevents user confusion and double submissions)
Provide clear, immediate feedback when users submit forms. Users should always know the current state: submitting, success, or failure.
Incorrect
<!-- ❌ Bad: No submission feedback -->
<form onsubmit="submitForm()">
<input type="email" name="email" required>
<button type="submit">Subscribe</button>
</form>
<script>
async function submitForm() {
// No loading indication
const response = await fetch('/api/subscribe', {
method: 'POST',
body: new FormData(form)
});
// Silent success - user doesn't know it worked
if (response.ok) {
console.log('Success');
}
// Unhelpful error
if (!response.ok) {
alert('Error');
}
}
</script><!-- ❌ Bad: Confusing states -->
<form>
<input type="email" name="email">
<!-- Button changes but no visual feedback -->
<button type="submit" id="btn">Subscribe</button>
</form>
<script>
btn.textContent = 'Sending...';
// User can click multiple times
// No disabled state
// Page reloads on error, losing context
</script>Problems:
- No loading indicator leaves users wondering if the form submitted
- Silent success provides no confirmation that the action worked
- Generic
alert('Error')gives no actionable guidance - Button is not disabled during submission, allowing double-clicks
- Page reload on error loses all entered data
Correct
<!-- ✅ Good: Comprehensive submission feedback -->
<form id="newsletter-form" novalidate>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-error"
autocomplete="email">
<p id="email-error" class="error" hidden></p>
</div>
<button type="submit"
id="submit-btn"
aria-describedby="submit-status">
<span class="btn-text">Subscribe</span>
<span class="btn-loading" hidden>
<svg class="spinner" aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="3"/>
</svg>
<span>Subscribing...</span>
</span>
</button>
<!-- Status message for screen readers -->
<div id="submit-status"
role="status"
aria-live="polite"
class="visually-hidden">
</div>
</form>
<!-- Success state (shown after successful submission) -->
<div id="success-message" class="success-panel" tabindex="-1" hidden>
<svg class="success-icon" aria-hidden="true" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"/>
</svg>
<h2>You're subscribed!</h2>
<p>Check your inbox for a confirmation email.</p>
<button type="button" onclick="resetForm()">Subscribe another email</button>
</div>
<script>
const form = document.getElementById('newsletter-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
const statusEl = document.getElementById('submit-status');
const successMessage = document.getElementById('success-message');
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Validate
const email = document.getElementById('email');
if (!email.validity.valid) {
showError(email, 'Please enter a valid email address');
return;
}
// Set loading state
setLoadingState(true);
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value })
});
const data = await response.json();
if (response.ok) {
showSuccess();
} else {
// Server returned an error
showError(email, data.message || 'Subscription failed. Please try again.');
setLoadingState(false);
}
} catch (error) {
// Network or other error
showError(email, 'Network error. Please check your connection and try again.');
setLoadingState(false);
}
});
function setLoadingState(isLoading) {
submitBtn.disabled = isLoading;
submitBtn.setAttribute('aria-busy', isLoading);
btnText.hidden = isLoading;
btnLoading.hidden = !isLoading;
// Announce to screen readers
if (isLoading) {
statusEl.textContent = 'Submitting your subscription...';
}
}
function showSuccess() {
form.hidden = true;
successMessage.hidden = false;
// Announce success
statusEl.textContent = 'Success! You have been subscribed to our newsletter.';
// Focus the success message for accessibility
successMessage.focus();
}
function showError(input, message) {
const errorEl = document.getElementById(`${input.id}-error`);
input.setAttribute('aria-invalid', 'true');
errorEl.textContent = message;
errorEl.hidden = false;
input.focus();
// Announce error
statusEl.textContent = `Error: ${message}`;
}
function resetForm() {
form.reset();
form.hidden = false;
successMessage.hidden = true;
document.getElementById('email').focus();
}
</script>
<style>
/* Loading spinner animation */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
}
.spinner circle {
stroke-dasharray: 50;
stroke-dashoffset: 20;
}
/* Button states */
button[type="submit"] {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-width: 140px;
justify-content: center;
}
button[type="submit"]:disabled {
opacity: 0.7;
cursor: wait;
}
/* Success panel */
.success-panel {
text-align: center;
padding: 2rem;
background: #f0fdf4;
border: 2px solid #22c55e;
border-radius: 8px;
}
.success-icon {
width: 48px;
height: 48px;
color: #22c55e;
}
/* Focus for success panel */
.success-panel:focus {
outline: 3px solid #22c55e;
outline-offset: 2px;
}
</style>Benefits:
- Loading state with disabled button prevents double submissions
- Screen reader users are notified of state changes via ARIA live regions
- Clear success confirmation builds user confidence
- Specific error messages with recovery guidance help users fix issues
- Form data is preserved on error so users do not need to retype
Reference: web.dev - Form Submission