Forms, content, and perception
Make errors recoverable
Identify the field, explain the problem, preserve input, and move or announce attention appropriately.
An error message only works if the user can find it, understand it, and fix it without starting over. That’s three separate things, and the flawed registration page gets all three wrong: it turns two borders red and prints “Invalid form” at the bottom. A screen reader user hears nothing. A user who can’t tell red from gray sees nothing either.
WCAG names this in 3.3.1 Error Identification (Level A): the field is identified and the error is described in text. 3.3.3 Error Suggestion (Level AA) adds that you should say how to fix it when you know.
Say what’s wrong, at the field
Start with the field itself. Mark it invalid and link it to a message that explains the fix:
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
value="anna.studio.it"
aria-invalid="true"
aria-describedby="email-error"
>
<p id="email-error">Enter an email address with an @, like [email protected]</p>
Focus the field with VoiceOver running and you hear “Email, invalid data, edit text, anna.studio.it, Enter an email address with an @, like [email protected]”. The name, the state, the current value and the fix, in one announcement. Notice the message shows a correct example instead of saying “invalid email”.
Keep the value filled with what the user typed. Wiping the field to punish a typo means retyping everything, which is where people give up.
Summarize when there’s more than one
With several errors the user needs an overview. Put a summary at the top of the form, make it focusable, and link each item to its field:
<div id="error-summary" tabindex="-1" role="group" aria-labelledby="error-heading">
<h2 id="error-heading">There are 2 problems with your registration</h2>
<ul>
<li><a href="#email">Enter an email address with an @, like [email protected]</a></li>
<li><a href="#arrival">Choose 14 or 15 October 2026 as your arrival day</a></li>
</ul>
</div>
After the page re-renders with errors, move focus to it:
const summary = document.querySelector('#error-summary')
if (summary) summary.focus()
tabindex="-1" lets the script focus a div without adding it to the Tab order. The user hears the heading, arrows through the list, and Enter on a link jumps to the broken field. Two problems, two jumps, done.
Server side too
Client validation is a convenience. The server is where the real check happens, and it has to return the same experience: the summary, the marked fields, and every valid value still in place. If your framework re-renders the form on a failed POST, pass the submitted values back into the template. A redirect to a clean page throws the user’s work away.
Test it the hard way
Fill in the form with five distinct mistakes: empty name, malformed email, no contact preference, no ticket, arrival on the wrong day. Submit with the keyboard. Then fix all five using only Tab, the summary links and a screen reader, and check that the fields you got right still hold their values at the end. The generic “Invalid form” message fails this run at step one: you don’t know which five.
Try this on your own form with the same five-mistake run. Every time you had to retype something you already entered, you found a barrier.
Lesson completed