Доступные формы

Форма — место, где доступность важнее всего: здесь пользователь что-то отправляет, и одна неподписанная кнопка может сорвать всё действие.

Доступная форма — форма, в которой каждое поле имеет понятную подпись, ошибки объясняются текстом, а заполнить её можно с клавиатуры и со скринридером.

label и for — связка подписи с полем

Главное правило доступных форм: у каждого поля должна быть подпись <label>, программно связанная с этим полем. Без связи скринридер при попадании в поле скажет лишь «поле ввода» — без намёка, что туда писать.

Связь делается через пару for + id: атрибут for у <label> указывает на id поля.

<label for="email">Электронная почта</label>
<input id="email" type="email" name="email">

Бонус для всех пользователей: клик по такой подписи переносит фокус в поле (а у чекбокса — переключает его). Кликабельная зона становится больше — удобно и на десктопе, и на телефоне.

Обёртка label

Есть второй способ связать подпись и поле — обернуть поле в <label>. Тогда for и id не нужны, связь возникает по вложенности:

<label>
  Я согласен с условиями
  <input type="checkbox" name="agree">
</label>

Оба способа корректны. Обёртку часто берут для чекбоксов и радиокнопок, а пару for/id — для текстовых полей, где подпись и поле верстаются отдельно. Что нельзя — оставить поле совсем без подписи. И placeholder подписью не является: серый текст-подсказка исчезает, как только начинаешь печатать, и плохо озвучивается.

fieldset и legend — группировка связанных полей

Иногда несколько полей образуют единый вопрос. Классика — группа радиокнопок: у каждой своя подпись («Картой», «Наличными»), но всем нужен общий вопрос («Способ оплаты»). Для этого есть <fieldset> (рамка-группа) и <legend> (общий заголовок группы):

<fieldset>
  <legend>Способ оплаты</legend>

  <label><input type="radio" name="pay" value="card"> Картой</label>
  <label><input type="radio" name="pay" value="cash"> Наличными</label>
</fieldset>

Скринридер при переходе на радиокнопку прочитает и legend, и подпись: «Способ оплаты, Картой, переключатель». Без fieldset пользователь услышит изолированное «Картой» и не поймёт, к какому вопросу это относится.

Обязательные поля

Чтобы пометить поле обязательным, используйте нативный атрибут required. Он не только включает встроенную проверку браузера, но и сообщается скринридеру как «обязательно»:

<label for="name">Имя</label>
<input id="name" name="name" required>

Важно не полагаться только на цвет или звёздочку. Если в дизайне обязательность показывают звёздочкой *, поясните её значение текстом («Поля со звёздочкой обязательны») — иначе незрячий или человек, не различающий цвета, не поймёт правило. Атрибут required доносит смысл независимо от визуального оформления.

Сообщения об ошибках через aria-describedby

Когда поле заполнено неверно, мало покрасить рамку в красный — цвет не воспринимается всеми. Нужно: показать текст ошибки, связать его с полем и пометить поле как ошибочное.

Текст ошибки привязывают к полю через знакомый по уроку об ARIA атрибут aria-describedby, а сам факт ошибки — через aria-invalid="true":

<label for="phone">Телефон</label>
<input id="phone" name="phone"
       aria-invalid="true"
       aria-describedby="phone-error">
<p id="phone-error">Введите номер в формате +7XXXXXXXXXX</p>

Теперь, попав в поле, скринридер объявит: «Телефон, поле ввода, неверно, Введите номер в формате...». Пользователь сразу понимает и что не так, и как исправить. Когда ошибка устранена, aria-invalid снимают (или ставят "false") из JavaScript.

Озвучивание ошибки в момент появления

Если ошибки появляются динамически после нажатия «Отправить», их полезно поместить в live-регион (aria-live="assertive" или role="alert"), чтобы скринридер прочитал их сразу, не дожидаясь, пока пользователь сам дойдёт до поля. role="alert" — это, по сути, готовый ассертивный live-регион.

<!-- role="alert" — скринридер озвучит текст, как только он появится -->
<p id="phone-error" role="alert">Введите номер в формате +7XXXXXXXXXX</p>

autocomplete — помощь браузера и пользователя

Атрибут autocomplete подсказывает браузеру, что это за поле, чтобы он мог предложить сохранённое значение (имя, адрес, e-mail). Для людей с моторными ограничениями или дислексией это огромная экономия сил: не нужно вручную набирать длинные данные.

<label for="fname">Имя</label>
<input id="fname" name="fname" autocomplete="given-name">

<label for="mail">Почта</label>
<input id="mail" type="email" name="mail" autocomplete="email">

<label for="tel">Телефон</label>
<input id="tel" type="tel" name="tel" autocomplete="tel">

Значения autocomplete стандартизированы (given-name, family-name, email, tel, street-address, postal-code и десятки других). Это часть WCAG: критерий «Identify Input Purpose» прямо требует помечать назначение полей через autocomplete.

Как это работает под капотом

Когда <label> связан с полем (через for/id или обёрткой), браузер при построении дерева доступности берёт текст подписи как имя поля. Скринридер читает именно его. aria-describedby добавляет к узлу описание (текст ошибки или подсказки), которое озвучивается после имени. aria-invalid выставляет в дереве состояние «ошибка», а required — состояние «обязательно». То есть все эти атрибуты не рисуют ничего нового на экране — они наполняют узел поля смыслом, который потом озвучивает скринридер. Без связи label↔поле имя у поля пустое, и весь остальной труд теряет смысл.

Частые ошибки

  • Placeholder вместо label. Подсказка исчезает при вводе и не считается подписью — поле остаётся безымянным.
  • label без связи. Текст рядом с полем есть, но нет for/id и нет обёртки — связи нет, скринридер подпись не прочитает.
  • Группы радиокнопок без fieldset/legend. Непонятно, к какому вопросу относятся варианты.
  • Ошибка только цветом. Красная рамка без текста и без aria-invalid — незрячий и не различающий цвета пользователь не узнает о проблеме.
  • Игнор autocomplete. Поля без него заставляют каждый раз вручную набирать имя, адрес, телефон.

Итоги

  • Каждое поле снабжайте <label> — через for/id или обёрткой; placeholder подписью не является.
  • Группы связанных полей (радиокнопки) объединяйте в <fieldset> с <legend>.
  • Обязательность задавайте атрибутом required, не полагаясь только на цвет или звёздочку.
  • Ошибки показывайте текстом, связывайте через aria-describedby и помечайте поле aria-invalid="true"; для динамики — role="alert".
  • Назначение полей помечайте autocomplete — это и помощь пользователю, и требование WCAG.
Проверьте себя
1. Почему placeholder не может заменить <label> у поля формы?
Aplaceholder работает только в старых браузерах
BТекст-подсказка исчезает, как только пользователь начинает печатать, и плохо озвучивается скринридером — поле остаётся фактически без подписи
Cplaceholder нельзя стилизовать
Dplaceholder виден только на мобильных устройствах
2. Зачем группе радиокнопок нужны <fieldset> и <legend>?
AДля красивой рамки вокруг полей
BЧтобы legend дал группе общий заголовок-вопрос, который скринридер прочитает вместе с подписью каждого варианта
CЧтобы радиокнопки стали обязательными
DОни нужны только для текстовых полей
3. Как доступно сообщить о неверно заполненном поле?
AДостаточно покрасить рамку поля в красный цвет
BПоказать текст ошибки, связать его с полем через aria-describedby и пометить поле aria-invalid="true"
CПросто не отправлять форму без объяснений
DПоставить полю атрибут required