Runnable lesson · press Try it on the examples to edit and run them here.
A validation message is useful only if the user notices it, understands it and can reach it, including users of screen readers and keyboards. This lesson covers the markup and the wording.
Connect the message to the field
Two ARIA attributes tie an error to its input:
aria-invalid="true"tells assistive technology the value is wrong.aria-describedby="id"points at the element with the message, which is read after the label.
function App() {
const [email, setEmail] = React.useState("");
const [checked, setChecked] = React.useState(false);
const valid = /^\S+@\S+\.\S+$/.test(email);
const showError = checked && !valid;
return (
<form noValidate onSubmit={(e) => { e.preventDefault(); setChecked(true); }}>
<label htmlFor="email">Email</label>
<input
id="email"
aria-label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={showError ? "true" : undefined}
aria-describedby={showError ? "email-error" : undefined}
/>
{showError && (
<p id="email-error" style={{ color: "#b71c1c", margin: "4px 0" }}>
Enter an email address like name@example.com
</p>
)}
<button type="submit">Check</button>
</form>
);
}
A screen reader will say something like “Email, edit text, invalid entry, Enter an email address like name@example.com”.
Announce errors that appear later
Content that appears dynamically is not announced unless it is in a live region. Use:
role="alert"for an important message that should interrupt (“Your payment failed”).role="status"for polite updates (“Saved”).- A visible error summary at the top of the form after a failed submit.
Move focus to the first problem
After a failed submit, send keyboard and screen-reader users to the first invalid field, so they do not have to hunt:
function App() {
const [name, setName] = React.useState("");
const [error, setError] = React.useState("");
const nameRef = React.useRef(null);
function handleSubmit(e) {
e.preventDefault();
if (!name.trim()) {
setError("Enter your name");
nameRef.current.focus(); // move focus to the field with the error
return;
}
setError("");
}
return (
<form noValidate onSubmit={handleSubmit}>
<label>
Name
<input
ref={nameRef}
aria-label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
aria-invalid={error ? "true" : undefined}
aria-describedby={error ? "name-error" : undefined}
/>
</label>
{error && <p id="name-error" role="alert">{error}</p>}
<button type="submit">Save</button>
</form>
);
}
For forms with several errors, focus the first invalid field and list all errors in a summary at the top, each linking to its field.
Do not rely on colour alone
Red text is not enough for people who cannot see the colour. Add an icon or the word “Error”, and keep sufficient contrast (at least 4.5:1 for normal text against the background).
Write helpful messages
| Not helpful | Helpful |
|---|---|
| “Invalid input” | “Enter a date like 25/12/2026” |
| “Error 422” | “That email address is already registered. Try signing in.” |
| “Password too weak” | “Use at least 12 characters. A short phrase works well.” |
| “Required” | “Enter your email address” |
| “You failed to enter a correct value” | “The postcode looks incorrect. Check it and try again.” |
Good messages say what is wrong, how to fix it, and do not blame the user. Show the rule (such as password length) before the user makes an error, in the hint text under the label.
Keep what the user typed
Never clear a field because it has an error. Let people correct the value instead of retyping everything.
Key points
- Use
aria-invalidandaria-describedbyto connect messages to inputs, and live regions for dynamic messages. - Move focus to the first error after a failed submit.
- Write messages that explain how to fix the problem, and never rely on colour alone.