The checkout form is where the store earns its money, and at the same time the thing that most often breaks accessibility. Every field must have a programmatic name - a string the screen reader speaks when the user lands on the field. This string is called the accessible name. Without it a person hears just edit text and does not know what to type: a name, a city, or a card number. A checkout page has a dozen such fields, and silence on any one of them stops the purchase.
The naive solution saves space: drop the labels and leave a hint inside the field - a placeholder. It looks clean, the mockup is approved. It seems that the grey text 'Email address' inside the field is the label, since the eye reads it fine.
This breaks right after the first character. The placeholder disappears the moment the user starts typing - and a person who was distracted midway through the form no longer remembers what this field is. Grey text almost always fails contrast, and some screen readers do not read a placeholder as the field's name. A hint inside the field is not a substitute for a label.
The mechanism is simple and reliable: a visible label element, tied to the field through the for attribute and the id. This association gives the field its accessible name (this is required by 1.3.1 and 4.1.2) and, as a bonus, widens the click target - clicking the label puts focus into the field. WCAG 3.3.2 explicitly requires a label or instruction for every element the user types into.
Explanatory text - format, example, length limit - is tied to the field through aria-describedby: the screen reader reads it after the name. A distinct strength is correct autocomplete tokens (email, given-name, postal-code): the browser fills in data from the profile, and for people with memory and motor impairments this saves effort. This is required by 1.3.5.
Errors must not be signalled by colour alone - a red border is invisible to a colour-blind person and to a screen reader (that violates 1.4.1 and 3.3.1). The error text is placed next to the field, tied through aria-describedby, the field is marked aria-invalid true, and the text itself is announced aloud through role alert. A good error does not merely report a problem, it suggests how to fix it (3.3.3): not 'invalid input' but 'enter an address in the format name@example.com'.
Related fields are joined into a group: delivery method or card type is a set of radio buttons that must share a common heading. That heading is given through fieldset and legend: the screen reader reads the legend before each button, and the user understands that 'courier' and 'pickup' are options of one question, not separate independent checkboxes.
Automatic validation must not take away what was entered: wiping the form on an error or forcing the user to retype an email they already entered on a previous step is cruel (this is what 3.3.7 and 3.3.4 are about). The submit button is blocked during the request so as not to create two orders, but the process and the result must be announced - otherwise the keyboard user has no idea whether anything was sent at all.
| Task | Solution |
|---|---|
| Group of fields | fieldset + legend |
| Hint | aria-describedby |
| Error | Text next to the field + a summary at the top of the form |
| Autofill | Correct autocomplete tokens |
| Submit state | Block repeat submits, but announce the process and the result |
A typical failure: the person reaches the last field, validation fires on the fly, the form highlights the error with a single red border and along the way erases the address already entered. The mouse user winces and retypes it, the screen reader user hears not a word about the error, loses the data, and leaves the cart. A form is checked the way a checkout counter is: walk the whole payment path with a screen reader and a keyboard, make a mistake on purpose, and confirm that the error is named and the data is intact.
<label for="email">Email address</label>
<p id="email-help">We will send a confirmation.</p>
<input
id="email"
name="email"
type="email"
autocomplete="email"
aria-describedby="email-help email-error"
aria-invalid="true"
/>
<p id="email-error" role="alert">
Enter an address in the format name@example.com
</p>