Implementation worksheet · 7 min read
An Accessible Form Error Specification for AI Builders
Write the error behaviour down before the form is generated, one row per error: the field and rule, where it is detected (browser, app code, API or database), the exact message, where it appears and how it is announced. Every error must identify the field and describe the problem in text (WCAG 2.2 3.3.1 Error Identification, Level A), and should say how to fix it when that is known and safe (3.3.3 Error Suggestion, AA). Each message is tied to its field in markup, and the spec picks one focus behaviour for a failed submit. Spell out server-side errors most carefully, such as an email rejected by a unique constraint: a prompt that describes only the happy path gives the builder nothing to map them to.
Scope: the actor is the person writing the prompt or spec for a data-entry form in an AI-generated app (a create-record form, an edit dialog, a signup or invite form) and the person who accepts it. The starting state is a form that works when every input is valid. The boundary is the form, its submit handler and the API response it receives; email deliverability and fraud rules sit outside it. The outcome is a spec the builder can implement and a tester can verify line by line, including the errors only the server knows about.
Put it into practice
1. Inventory each rule and the layer that enforces it
For every field, list its rules and where each is checked: required and format checks can run in the browser, business rules in app code, uniqueness and reference rules in the database. A rule enforced only on the server still needs a message on the form. Do this first, because the builder can only map the errors it has been told exist.
2. Write each message as the fix
'Enter an email address like name@example.com' beats 'Invalid input'. 3.3.1 requires the item in error to be identified and the error described in text; 3.3.3 asks for the correction when it is known. 3.3.3 also exempts suggestions that would jeopardise security, which is why a sign-in form can say the email and password don't match without saying which one was wrong.
3. Tie every message to its field in markup
Put the message next to the field, reference it from the input with aria-describedby, and set aria-invalid='true' only once validation has run. W3C technique ARIA21 describes this pattern and says aria-invalid should not be set to true before validation is performed. Colour can't carry the error on its own (1.4.1 Use of Color, A): a red border needs text beside it.
4. Choose one focus behaviour for a failed submit
Either move focus to an error summary at the top that states the number of problems and links to each field (W3C lists a mechanism to jump to errors as advisory technique G139), or move focus to the first invalid field. The spec has to rule out the silent version, where the button is pressed, focus stays on it, and errors appear somewhere the user isn't looking.
5. Announce errors that appear without a focus change
An error injected while focus stays put (inline validation when a field loses focus, or a 'Couldn't save' banner after an API error) is a status message under 4.1.3 Status Messages (AA). It needs a role or live region so a screen reader reports it without the user hunting. If focus moves to the summary instead, the move does the announcing.
6. Map every server error to a field or to the summary
Specify the API's error shape and where each error lands. For example: a conflict response for a duplicate email goes to the email field as 'This person is already in the workspace'; a validation response naming two fields marks both; a 5xx or a timeout goes to the summary with a retry button, and everything the user typed stays in the form. Losing typed input on a server error forces the user to start again, and it never shows up in a happy-path demo.
7. Keep what the user entered and don't ask twice
After an error, every valid field keeps its value. In a multi-step form, information entered earlier in the same process shouldn't be requested again unless re-entry is essential or needed for security (3.3.7 Redundant Entry, A). On sign-in, don't block paste or password managers: 3.3.8 Accessible Authentication (Minimum) (AA) names both as ways to avoid relying on memory.
8. Worked example (illustrative, synthetic data)
A generated Invite teammate form with three fields: name (required), email (required, unique per workspace) and role (required select). First submission: name blank, email 'sam.example.com', role Editor. Expected: focus moves to a summary reading 'There are 2 problems with this form' with two links; the name field shows 'Enter a name'; the email field shows 'Enter an email address like sam@example.com'; role still shows Editor. Second submission, with a valid email that already belongs to a workspace member: the API returns a conflict, the email field shows 'This person is already in the workspace', name and role keep their values, and the summary now says 1 problem.
Form error specification worksheet
Copy this structure into your review document and record your observed result for each row.
| Error case | Detected by | Message (example) | Where it appears and how it is announced | WCAG 2.2 reference |
|---|---|---|---|---|
| Required name left blank | Browser or app code on submit | Enter a name | Under the field and in the error summary, which takes focus | 3.3.1 Error Identification (A) |
| Email missing the @ | App code when the field loses focus, and on submit | Enter an email address like name@example.com | Under the field; announced as a status message | 3.3.1 (A); 3.3.3 Error Suggestion (AA); 4.1.3 Status Messages (AA) |
| Email already in the workspace | Database unique constraint, returned by the API | This person is already in the workspace | Under the email field once the response arrives; summary count updates | 3.3.1 (A) |
| No role chosen | Browser or app code on submit | Choose a role | Under the select and in the summary | 3.3.1 (A); 3.3.2 Labels or Instructions (A) |
| Text longer than the column allows | App code, matching the database column length | Use 80 characters or fewer (currently 93) | Under the field | 3.3.1 (A); 3.3.3 (AA) |
| Due date in the past | App code | Choose today or a later date | Under the date field | 3.3.1 (A); 3.3.3 Error Suggestion (AA) |
| Several errors at once | Any combination of the above | There are 2 problems with this form | Summary at the top with a link to each field; summary takes focus | 3.3.1 (A) |
| Server error or timeout on save | API 5xx or no response | We couldn't save this. Your entries are still here. Try again. | Summary with a retry button; live region if focus stays put | 4.1.3 Status Messages (AA) |
| Sign-in details don't match | Server | That email and password don't match | Summary; neither field singled out | 3.3.1 (A); 3.3.3 security exception (AA) |
| Invalid state shown by colour only | Design review | Add the text message beside the red border | Next to the field | 1.4.1 Use of Color (A) |
A failure worth checking
The disabled submit button. A generated form greys out Save until every field is valid. It looks tidy, and it removes the errors from the experience: nothing tells the user which field is holding the button back, so there is no text identifying the problem, and a disabled button can't take keyboard focus, so someone tabbing through the form skips straight past it. Validating on every keystroke is the opposite mistake, announcing 'invalid email' while someone is still typing; validate when the field loses focus and on submit. The limit of the spec itself: it fixes what the form says and where, not whether the wording makes sense to your users, which only watching them use it will tell you.
Common questions
Should errors show inline, in a summary, or both?
Both, for forms with more than a couple of fields: the summary says how many problems there are and links to each, and the inline message sits where the fix happens. For a one-field form, such as a search box or a single email invite, the inline message on its own is enough.
Can I just ask the builder to make the form accessible?
You can, but you won't be able to check the result, because the request has no pass condition. Paste the worksheet rows into the request instead. Each row is a behaviour a tester can verify, and a missing row is a gap you can see before the form ships.
What does this spec not cover?
Contrast of the error text, zoom and reflow, and how the form behaves inside a dialog. Test those separately; the modal focus test covers the last one.
Basis and scope
This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.