Доступные формы
Форма — самое хрупкое место сайта: именно здесь пользователь уходит, если разметка сделана «на глаз».
Доступная форма — форма, в которой у каждого поля есть программно связанная подпись, обязательность и ошибка выражены не только цветом и рамкой, а сообщение об ошибке доходит до человека, даже если он не смотрит на экран.
Зачем это на практике
Представьте незрячего пользователя, который оформляет заказ. Он идёт по полям клавишей Tab, а скринридер (программа экранного доступа) читает вслух то, что «видит» в дереве доступности: имя поля, тип, состояние. Если подписи нет, он услышит: «поле ввода, редактирование». И всё. Какое поле? Индекс? Квартира? Отчество? Он вернётся, попробует угадать по контексту — и в половине случаев просто уйдёт.
Формы — это точка, где сайт зарабатывает: регистрация, оплата, заявка, подписка. Поэтому дефект доступности в форме бьёт по деньгам напрямую, а не «когда-нибудь при проверке». И заметьте: правильная разметка помогает не только незрячим. Подпись, связанная с полем, увеличивает область попадания — клик по слову «Согласен» ставит галочку в чекбокс. Это выигрыш для человека с тремором рук, для того, кто держит телефон одной рукой в метро, и вообще для всех.
label и связь с полем — ошибка номер один
Самый частый дефект доступности выглядит абсолютно невинно: текст рядом с полем есть, а программной связи между ними нет.
Плохо:
<p>Электронная почта</p>
<input type="email" name="email">
Мы, зрячие, связываем текст и поле близостью. Браузер так не умеет: для него это просто абзац и, отдельно, безымянный инпут.
Хорошо, способ 1 — for и id:
<label for="email">Электронная почта</label>
<input id="email" type="email" name="email" autocomplete="email">
Хорошо, способ 2 — обёртка (когда проставить уникальный id неудобно):
<label>
Электронная почта
<input type="email" name="email" autocomplete="email">
</label>
По умолчанию выбирайте первый способ: он надёжнее в старых скринридерах и не мешает вёрстке.
Способ 3 — aria-label, когда видимой подписи нет по дизайну (строка поиска с иконкой лупы):
<input type="search" name="q" aria-label="Поиск по сайту">
placeholder — это не подпись
Соблазн велик: серый текст внутри поля выглядит чисто, дизайнер доволен. Но placeholder проваливается сразу по четырём пунктам:
- он исчезает при вводе — проверить, что именно ты заполнил, уже нельзя;
- у него по умолчанию низкий контраст, и часто он не проходит даже минимальные требования;
- скринридеры обрабатывают его по-разному: кто-то молчит, кто-то читает вместо имени;
- его ломает автозаполнение и автоперевод страницы.
Вывод простой: placeholder — это пример формата, а не подпись. «+7 (999) 000-00-00» — уместно. «Телефон» — нет.
| Приём | Когда использовать | Подпись видна? |
<label for="…"> | по умолчанию, в 90% случаев | да |
<label>-обёртка | когда нельзя проставить id | да |
aria-label | подписи нет по дизайну (иконка-поиск) | нет |
aria-labelledby | подпись уже есть в другом элементе (заголовок, ячейка) | да |
Подсказки и обязательность
Подпись отвечает на вопрос «что это за поле». Подсказка — на вопрос «как его заполнить». Связывать её нужно через aria-describedby: скринридер прочитает её после имени поля.
<label for="pwd">Пароль</label>
<input id="pwd" type="password" autocomplete="new-password"
aria-describedby="pwd-hint">
<p id="pwd-hint">Минимум 8 символов, хотя бы одна цифра.</p>
Ключевое отличие от aria-label: описание дополняет имя, а не заменяет его. Пользователь услышит «Пароль, поле ввода, минимум 8 символов, хотя бы одна цифра».
Обязательность передаёт атрибут required: он и валидирует форму, и попадает в дерево доступности как состояние «обязательно». Звёздочка в подписи — визуальный дубль, и его обязательно нужно расшифровать в начале формы: «Поля со звёздочкой обязательны». Если вы валидируете сами и required не ставите, добавьте aria-required="true".
Приём, который экономит всем нервы: если из восьми полей семь обязательные, помечайте необязательные. «(необязательно)» рядом с одним полем читается лучше, чем семь звёздочек.
И не забывайте про autocomplete. Это тоже доступность (критерий WCAG 1.3.5): браузер подставит имя, адрес, телефон, и человеку с когнитивными трудностями или нарушением моторики не придётся печатать всё вручную.
Сообщения об ошибках, которые реально доходят
Типичная картина: поле краснеет, под ним появляется текст, страница остаётся на месте. Для зрячего пользователя — нормально. Для скринридера — не произошло ничего. Фокус остался на кнопке «Отправить», новый текст в DOM его никак не касается, а красная рамка — это CSS, которого в дереве доступности не существует в принципе.
Чтобы ошибка дошла, нужны три вещи:
aria-invalid="true"на поле — состояние «здесь ошибка»;- текст ошибки, связанный с полем через
aria-describedby— чтобы он читался вместе с полем при возврате к нему; - «живая область» (
role="alert") — чтобы сообщение объявили сразу, как только оно появилось.
<label for="inn">ИНН</label>
<input id="inn" name="inn" inputmode="numeric"
aria-invalid="true" aria-describedby="inn-error">
<p id="inn-error" role="alert">ИНН состоит из 10 или 12 цифр. Вы ввели 9.</p>
Что услышит пользователь:
ИНН, поле ввода, недопустимое значение.
ИНН состоит из 10 или 12 цифр. Вы ввели 9.
Отдельно про сам текст. «Ошибка», «Неверное значение», «Поле заполнено некорректно» — сообщения-пустышки: они констатируют факт и не помогают. Хорошее сообщение говорит, что не так и что сделать: «Дата в формате ДД.ММ.ГГГГ, например 05.09.1998».
Сводка ошибок сверху
Для длинных форм после отправки показывайте сводку и переводите на неё фокус:
<div id="form-errors" role="alert" tabindex="-1">
<h2>Форма не отправлена: 2 ошибки</h2>
<ul>
<li><a href="#email">Электронная почта: адрес без символа @</a></li>
<li><a href="#inn">ИНН: должно быть 10 или 12 цифр</a></li>
</ul>
</div>
tabindex="-1" делает контейнер фокусируемым программно (через .focus()), но не добавляет его в обход по Tab. Пользователь сразу узнаёт, сколько ошибок, и по ссылке прыгает к нужному полю. Такая сводка — единственное, что спасает в форме на 30 полей.
И анти-совет, который встречается повсеместно: не блокируйте кнопку «Отправить», пока форма невалидна. Отключённая кнопка ничего не объясняет, часто не получает фокус и превращается в тупик — человек видит серую кнопку и не понимает, что от него хотят. Пусть кнопка всегда нажимается, а по клику появляется сводка.
Группировка: fieldset и legend
Одиночной подписи достаточно, пока поле самодостаточно. Но у радиокнопок подпись есть у каждой опции, а вопрос — общий:
<fieldset>
<legend>Способ доставки</legend>
<label><input type="radio" name="delivery" value="pickup"> Самовывоз</label>
<label><input type="radio" name="delivery" value="courier"> Курьер</label>
</fieldset>
Скринридер прочитает: «Способ доставки, Самовывоз, переключатель, 1 из 2». Без fieldset он скажет только «Самовывоз, переключатель» — и человек не понимает, на какой вопрос отвечает.
Те же правила работают для группы чекбоксов («Что вас интересует») и для смысловых блоков длинной формы: «Данные получателя», «Адрес доставки», «Оплата». Если fieldset мешает вёрстке (у него есть свои особенности отображения), эквивалент — role="group" плюс aria-labelledby, указывающий на заголовок блока.
Как это работает
Браузер строит из DOM дерево доступности (accessibility tree) — упрощённую копию страницы, где у каждого узла есть роль, имя, состояние и значение. Скринридер, голосовое управление и брайлевский дисплей читают именно его, а не ваш красивый CSS.
Имя элемента (accessible name) вычисляется по строгому алгоритму, приоритет сверху вниз:
aria-labelledby— побеждает всё;aria-label;- связанный
<label>(черезfor/idили обёртку); - запасные варианты вроде
titleиplaceholder— последняя надежда, вести себя могут по-разному.
Отсюда важнейшее практическое следствие: aria-label молча перебивает видимую подпись. Если в разметке <label>Телефон</label>, а на инпуте висит aria-label="phone", пользователь услышит «phone». Ломается и голосовое управление: человек говорит «нажми Телефон», а система ищет элемент с именем «phone» и не находит. Это отдельный критерий WCAG 2.5.3 «Label in Name»: доступное имя должно содержать видимый текст.
Состояния (required, aria-invalid, disabled) кладутся в то же дерево. Поэтому менять их нужно атрибутами и ARIA, а не классами CSS: класс .is-error в дерево доступности не попадает вообще.
Частые ошибки
| Ошибка | Чем плоха | Как правильно |
placeholder вместо подписи | исчезает при вводе, низкий контраст, нестабильно читается | <label>, а placeholder — только под пример формата |
| Красная рамка без текста | смысл передан только цветом; для скринридера — ничего | aria-invalid + текст ошибки + aria-describedby |
Сообщение об ошибке появляется без role="alert" | текст в DOM есть, но его никто не объявляет | живая область или перевод фокуса на сводку |
<div onclick> вместо кнопки | не фокусируется, не жмётся Enter/Space, роли нет | <button type="submit"> |
aria-label, не совпадающий с видимой подписью | ломает голосовое управление (WCAG 2.5.3) | доступное имя включает видимый текст |
Кнопка «Отправить» disabled до валидации | тупик: не объясняет, что не так | кнопка активна, после клика — сводка ошибок |
Радиогруппа без fieldset | непонятно, на какой вопрос отвечает выбор | fieldset + legend |
Чек-лист доступной формы
[ ] У каждого поля есть label с for/id (или aria-label, если подписи нет по дизайну)
[ ] placeholder не подменяет подпись
[ ] Обязательность передана через required / aria-required, а не только звёздочкой
[ ] Подсказки связаны через aria-describedby
[ ] У поля с ошибкой стоит aria-invalid="true"
[ ] Текст ошибки связан с полем и объявляется (role="alert")
[ ] Текст ошибки говорит, ЧТО сделать, а не только «неверно»
[ ] Радио- и чекбокс-группы обёрнуты в fieldset + legend
[ ] Проставлен autocomplete там, где это персональные данные
[ ] Форму можно заполнить и отправить, не трогая мышь
Итоги
- Подпись без программной связи — не подпись.
for/idили обёртка<label>— по умолчанию. placeholder— пример формата, а не имя поля. Никогда не заменяйте имlabel.- Подсказка — через
aria-describedby: она дополняет имя, аaria-labelего заменяет. - Ошибка доходит только при сочетании трёх вещей:
aria-invalid+ связанный текст +role="alert"(или перевод фокуса на сводку). - Радиокнопки и чекбоксы — в
fieldsetсlegend, иначе выбор теряет вопрос. - Всё, что вы выражаете классами CSS, в дереве доступности не существует. Состояния — атрибутами.