Style Form Errors Users Can Actually Notice
Build and test a local email validation flow that explains errors, preserves the typed value and clears stale error states after correction.
An error needs to tell the reader what to fix, stay connected to the field and disappear when it no longer applies. A red border alone does none of those jobs reliably. Start with one field so you can test the entire transition.
This is a local validation demonstration. It makes no network request and does not check whether a mailbox exists. Save the file as email-errors.html and use fictitious addresses.
Run the complete validation flow
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>An error the user can fix</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 16px;
font: 1rem/1.6 system-ui, sans-serif;
color: #080b0e;
background: #ffffff;
}
main { max-width: 60rem; margin-inline: auto; }
form { max-width: 32rem; }
label { display: block; font-weight: 700; }
input, button { font: inherit; padding: 0.65rem; }
input { width: 100%; border: 2px solid #626976; }
input[aria-invalid="true"] { border-color: #b42318; }
.error { color: #b42318; }
:is(input, button):focus-visible {
outline: 3px solid #034bff;
outline-offset: 3px;
}
</style>
</head>
<body>
<main>
<h1>Check an email locally</h1>
<form id="email-form" novalidate>
<label for="email">Email address</label>
<p id="email-hint">Use a fictitious address for this example.</p>
<input id="email" name="email" type="email" required
autocomplete="email" aria-describedby="email-hint">
<p id="email-error" class="error" hidden></p>
<p><button type="submit">Check address</button></p>
<p id="status" role="status"></p>
</form>
</main>
<script>
const form = document.querySelector('#email-form');
const email = document.querySelector('#email');
const error = document.querySelector('#email-error');
const status = document.querySelector('#status');
let attempted = false;
function validate() {
let message = '';
if (email.validity.valueMissing) message = 'Enter your email address.';
else if (email.validity.typeMismatch) message = 'Enter an address such as reader@example.com.';
error.textContent = message;
error.hidden = !message;
if (message) {
email.setAttribute('aria-invalid', 'true');
email.setAttribute('aria-describedby', 'email-hint email-error');
} else {
email.removeAttribute('aria-invalid');
email.setAttribute('aria-describedby', 'email-hint');
}
return !message;
}
form.addEventListener('submit', event => {
event.preventDefault();
attempted = true;
status.textContent = '';
if (!validate()) email.focus();
else status.textContent = 'Basic format accepted locally. Nothing was sent.';
});
email.addEventListener('input', () => {
status.textContent = '';
if (attempted) validate();
});
</script>
</body>
</html>
The field has a persistent label and hint. Initially there is no error border or message. The form uses novalidate so the submit handler can present the browser's basic validity results through this example's own interface. The underlying required/email checks still expose their validity flags.
Test the first unsuccessful submission
- Leave the field empty and activate Check address.
- Confirm the message asks for an email address, the border changes, and focus moves to the field.
- Enter
readerand submit again. The message should explain the expected address format. - Confirm your typed value remains. The handler should not clear it and force you to start again.
The input's aria-describedby refers to both its normal hint and the visible error while invalid. The label remains its name. This gives assistive technology a relationship to the explanation instead of leaving a disconnected paragraph nearby.
Correct the value and remove stale feedback
After an attempted submission, type reader@example.com. The error text should disappear and aria-invalid should be removed. Submit the corrected value: a local status message appears. That message says what actually happened; it does not claim that an email was delivered.
Delete the field's content again. The old success message clears and the required-field error returns. Rechecking on input is gated behind the first submission attempt, so a new visitor is not greeted by an error before doing anything.
Check keyboard and assistive-technology behavior
Tab to the field and button, then submit with Enter. Keep the visible focus outline even while the error border is present. Error colour and keyboard position communicate different information.
The success paragraph is a status region. The error is associated with the focused input; it is not also an assertive alert in this example. Actual announcements vary with browser and assistive technology, so test the flow with the combinations you support. Inspecting ARIA attributes alone does not prove the listening experience.
Find the likely implementation mistake
If the page reloads, confirm the submit handler runs and prevents the default action. If the browser's own validation bubble appears before your handler, check the form's novalidate attribute. If the border clears but an old error is still read, inspect both the message visibility and described-by references.
Client-side checks are feedback helpers. A production endpoint still needs server-side validation, authorization where relevant and its own handling of delivery failures. Do not replace those responsibilities with this example.
Change the message without breaking the relationship
Rewrite the missing-value message in plain language. Keep the error element's ID and the input references intact. Repeat the empty, malformed, corrected and edited-after-success cases.
Expected result: only the wording changes. The value stays intact, keyboard focus is visible, invalid state clears after correction and no request leaves the page. For a larger form with native validation, continue to labels and grouped controls.
About
Build and test a local email validation flow that explains errors, preserves the typed value and clears stale error states after correction.
- Responsible
- Mary Q
- Last Update
- 08/27/2026
- Reading Time
- 5 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.