A form can look finished while its most important behaviour is still undefined. What happens when the network fails? What does a second tap do? Does the success message mean the server accepted the enquiry, or only that JavaScript ran?
Treating the form as a small state machine makes those questions easier to answer. This walkthrough covers a contact form, but the same approach works for demo requests and project enquiries.
Start with four states
Use idle, submitting, success, and error. Browser validation happens before the submit handler runs. The handler then moves into submitting; only the server response determines whether it can move into success.
| State | Submit button | Message | User input |
|---|---|---|---|
| Idle | Available | Brief instructions | Editable |
| Submitting | Disabled | Sending | Preserved |
| Success | Available | Request accepted | Clear only after acceptance |
| Error | Available | Actionable retry message | Preserved |
A disabled button reduces accidental double taps. It does not provide server-side duplicate protection: that requires an idempotency mechanism or another deliberate backend rule.
Keep the HTML useful before JavaScript
Give every field a visible label. Use autocomplete tokens to make mobile input less tedious. A telephone field helps the keyboard, but it does not establish that a number is valid or reachable.
<form action="/enquiries" method="post" id="enquiry-form">
<label for="contact-name">Your name</label>
<input id="contact-name" name="name"
autocomplete="name" required maxlength="100">
<label for="contact-phone">Phone number</label>
<input id="contact-phone" name="phone" type="tel"
autocomplete="tel" required maxlength="30">
<label for="project">What would you like to build?</label>
<textarea id="project" name="project"
rows="3" required maxlength="2000"></textarea>
<button type="submit">Send enquiry</button>
<p role="status" aria-live="polite" id="form-status"></p>
</form>
The action is illustrative: a real application needs a matching server route. That route should support ordinary form submission as well as the enhanced JSON response. Add the application's CSRF protection, server validation, abuse controls, and appropriate privacy information before deployment.
Define what acceptance means
For an enhanced request, establish a response contract such as:
{ "accepted": true }
The server should return this only after it has durably recorded or queued the enquiry according to the application's design. An accepted enquiry is not proof that an email reached an inbox, a salesperson responded, or a sale occurred.
const form = document.querySelector('#enquiry-form');
const button = form.querySelector('button[type="submit"]');
const status = document.querySelector('#form-status');
let submitting = false;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (submitting) return;
submitting = true;
button.disabled = true;
status.textContent = 'Sending your enquiry…';
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form)
});
const result = await response.json();
if (!response.ok || result.accepted !== true) {
throw new Error('Enquiry was not accepted');
}
status.textContent = 'Your enquiry has been received.';
form.reset();
} catch {
status.textContent =
'We could not confirm receipt. Please check your connection or contact us.';
} finally {
submitting = false;
button.disabled = false;
}
});
fetch() does not reject just because the server returns an HTTP error. The explicit response.ok check matters. Malformed or unexpected JSON also goes through the error path here.
A lost response creates an awkward case: the server might have accepted the request even though the browser cannot confirm it. Preserve the input and use honest wording. For stronger retry guarantees, implement a server-recognised idempotency key rather than assuming a second request is safe.
Test the unsuccessful paths too
Before launch, check an empty submission, invalid input, a slow response, a server error, and a rapid double tap. Verify keyboard navigation, visible focus, readable labels, and a success message announced without moving focus unexpectedly. Use synthetic contact details in a test environment.
If analytics is added, place the enquiry event after confirmed acceptance and honour the site's measurement preferences. Do not put contact details or the free-text message into an analytics payload. A button click should remain a separate interaction event.
For the business-side decisions behind the fields, the EHUB mobile enquiry-form planning guide covers what to agree before implementation.
Technical references: HTML forms and Fetch response handling.
Published by EHUB. Prepared with AI assistance.
Top comments (0)