This form marks invalid fields with red text and a red border. What has to change?
Colour is not a signal on its own, so each error needs text, and that text must be programmatically tied to the field with aria-describedby and marked with aria-invalid. On submit, move focus to a summary or the first invalid field rather than hoping a live region is announced.
What the interviewer is scoring
- Does the candidate separate identifying the error, describing the fix, and announcing it, rather than treating them as one task
- Whether aria-describedby is chosen for the association and they can say why an adjacent span is not enough
- That they know aria-invalid changes what is announced and changes no behaviour
- Whether the submit path moves focus deliberately instead of relying on a live region firing
- Can they name the specific success criteria a reviewer would cite, not just say "it fails WCAG"
Answer
Three jobs, and red text does part of one
An error state has to do three separate things, and conflating them is how forms end up like this. It has to identify which field is wrong, describe what to do about it, and announce that something happened at all. Colour attempts only the first and fails it for anyone who cannot perceive the hue, which is why WCAG 1.4.1 Use of Color exists: colour may reinforce a distinction but may never be the only means of conveying it. So the first change is that every error acquires text, and every red border acquires an icon or a text marker beside the label.
The text also has to say something useful. Success criterion 3.3.1 asks only that the error be identified in text; 3.3.3 Error Suggestion, at level AA, asks that you suggest a correction when you know one. "Invalid date" satisfies neither well. "Enter the date as DD/MM/YYYY, for example 14/03/2026" satisfies both and removes a support ticket.
Associate the message, do not merely place it
Sighted users connect a message to a field by proximity. A screen reader user moving through the form by field does not encounter proximity at all — they hear the field's accessible name, its role, its state, and its description, and a <span> sitting next to the input is in none of those. aria-describedby is what puts it in the description, and it accepts multiple ids so hint text and error text can both be attached.
<label for="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-invalid="true"
/>
<p id="email-hint">We use this for order updates only.</p>
<p id="email-error">Enter an email address in the format name@example.com</p>
Two details in that markup are load-bearing. The label is associated by for, not by wrapping and hoping; a placeholder is not a label and disappears the moment the user types. And aria-describedby references ids that must exist in the DOM at the moment the field receives focus, which means if you render error paragraphs conditionally, the reference is dangling whenever the field is valid. Referencing an absent id is silently ignored, so this fails quietly rather than loudly.
aria-invalid announces, it does not validate
aria-invalid="true" causes assistive technology to announce the field as invalid alongside its name. That is all it does. It applies no constraint, blocks no submission, and adds no styling, though it is a good CSS hook precisely because it is the state of record: styling on [aria-invalid="true"] cannot drift out of sync with what is announced. Remove it, do not set it to "false" and leave it, once the field is corrected.
aria-errormessage exists for exactly this purpose and reads better in the spec, but support across screen readers has been uneven for long enough that aria-describedby remains the reliable choice. If you use aria-errormessage, it is only honoured while aria-invalid is true, which surprises people.
On submit, move focus
The instinct is to put the error summary in a live region and let it be read out. Live regions are the least dependable mechanism in the accessibility toolbox: the container must already exist in the DOM before content is inserted, insertion has to be a genuine mutation rather than a re-render that replaces the node, role="alert" interrupts whatever is currently being spoken, and behaviour still varies by screen reader and browser pairing.
Moving focus is deterministic. Render a summary at the top of the form listing each error as a link to its field, give the container tabindex="-1", and focus it after a failed submit. The user hears the count and the list, can activate a link to jump to a field, and — because focus moved — is now positioned at the top of the form rather than wherever the submit button was. For a single-field error, focusing that field directly is fine and less ceremony.
if (errors.length) {
summaryRef.current.focus(); // tabindex="-1" makes this focusable
}
Whatever you do, do not disable the submit button until the form is valid. A disabled button is removed from the tab order and gives no explanation, so a keyboard user reaches the end of the form and finds nothing to do and no reason why. Let the submit happen and report what is wrong.
What a reviewer will cite
Naming criteria precisely is what distinguishes an answer from an opinion. This form fails 1.4.1 Use of Color while colour is the only marker, and 3.3.1 Error Identification if the error is not identified in text. It fails 3.3.3 Error Suggestion where a correction is knowable and not offered, and 1.4.3 Contrast if the red on white does not reach 4.5:1 — which a lot of error red does not. WCAG 2.2, published in October 2023, adds two more that touch forms directly: 3.3.7 Redundant Entry, which asks you not to make the user re-enter information they already gave in the same process, and 3.3.8 Accessible Authentication, which rules out cognitive puzzles as the only route through a login.
Likely follow-ups
- Native constraint validation gives you error bubbles for free. Why do teams switch them off with novalidate?
- Where should the error message go visually, above or below the field, and what does that have to do with zoom?
- A field becomes invalid while the user is still typing in it. When do you announce that, and how do you avoid interrupting them?
- How would you handle a server-side error that applies to the whole form rather than to one field?
Related questions
- Design rejected the native control, so you are building a custom one. How do you make it accessible?hardAlso on accessibility and wcag5 min
- Your endpoint takes a typed request body and it still crashed on a missing field. How is that possible when it compiled?mediumAlso on validation4 min
- How do you know a problem is real before you build anything to solve it?mediumAlso on validation4 min
- Where does TypeScript stop protecting you, and what do you do at those points?hardAlso on validation4 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: concept6 min
- A client disconnects but your server keeps working on their request. How does cancellation actually propagate in .NET?mediumSame kind of round: concept4 min
- How do goroutines leak, and how would you find a leak in a running service?mediumSame kind of round: concept5 min
- How do you split a story that is too big to fit in a Sprint?mediumSame kind of round: concept3 min