Форма оформления заказа - место, где магазин зарабатывает, и одновременно то, что чаще всего ломает доступность. Каждое поле должно иметь программное имя - строку, которую диктор произнесёт, когда пользователь встанет на поле. Это имя называют доступным именем. Без него человек слышит просто edit text и не знает, что вписывать: имя, город или номер карты. На странице оформления таких полей десяток, и молчание любого из них останавливает покупку.
Наивное решение экономит место: убрать подписи и оставить подсказку внутри поля - placeholder. Выглядит чисто, макет одобряют. Кажется, что серый текст 'Электронная почта' внутри поля и есть подпись, ведь глазами всё читается.
Ломается это сразу после первого символа. Placeholder исчезает, как только пользователь начинает печатать, - и человек, отвлёкшийся на середине формы, уже не помнит, что это за поле. Серый текст почти всегда не проходит по контрасту, а часть дикторов не читает placeholder как имя поля. Подсказка внутри поля - не замена подписи.
Механизм прост и надёжен: видимый элемент label, связанный с полем через атрибут for и id. Такая связь даёт полю доступное имя (это требование 1.3.1 и 4.1.2), а заодно расширяет область клика - нажатие по подписи ставит фокус в поле. WCAG 3.3.2 прямо требует подпись или инструкцию для каждого элемента, куда пользователь вводит данные.
Поясняющий текст - формат, пример, ограничение по длине - связывают с полем через aria-describedby: диктор прочитает его вслед за именем. Отдельная сила - правильные токены autocomplete (email, given-name, postal-code): браузер подставит данные из профиля, а людям с нарушениями памяти и моторики это экономит силы. Это требование 1.3.5.
Об ошибках нельзя сообщать одним цветом - красная рамка невидима для дальтоника и для диктора (это нарушает 1.4.1 и 3.3.1). Текст ошибки ставят рядом с полем, связывают через aria-describedby, помечают поле aria-invalid true, а сам текст объявляют вслух через role alert. Хорошая ошибка не просто сообщает о проблеме, но подсказывает, как её исправить (3.3.3): не 'неверный ввод', а 'укажите адрес в формате name@example.com'.
Связанные поля объединяют в группу: способ доставки или тип карты - это набор радиокнопок, у которого должен быть общий заголовок. Его дают через fieldset и legend: диктор прочитает legend перед каждой кнопкой, и пользователь поймёт, что 'курьер' и 'самовывоз' - варианты одного вопроса, а не отдельные независимые чекбоксы.
Автоматическая валидация не должна отнимать введённое: обнулять форму при ошибке или требовать заново вписать email, который человек уже вводил на прошлом шаге, - жестоко (об этом 3.3.7 и 3.3.4). Кнопку отправки на время запроса блокируют, чтобы не создать два заказа, но процесс и результат обязательно объявляют - иначе пользователь клавиатуры не поймёт, отправилось ли вообще.
| Задача | Решение |
|---|---|
| Группа полей | fieldset + legend |
| Подсказка | aria-describedby |
| Ошибка | Текст рядом с полем + сводка в начале формы |
| Автозаполнение | Корректные autocomplete tokens |
| Состояние отправки | Заблокировать повтор, но объявить процесс и результат |
Типичный провал: человек доходит до последнего поля, валидация срабатывает на лету, форма подсвечивает ошибку одной красной рамкой и заодно стирает уже введённый адрес. Пользователь мыши поморщится и повторит, пользователь диктора не услышит об ошибке ни слова, потеряет данные и уйдёт из корзины. Форму проверяют так же, как кассу: пройти весь путь оплаты с диктором и клавиатурой, ошибиться нарочно и убедиться, что ошибка названа и данные целы.
<label for="email">Электронная почта</label>
<p id="email-help">Отправим подтверждение.</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">
Укажите адрес в формате name@example.com
</p>