Доступность форм
Доступность (a11y) — важная часть формы. Хорошая форма удобна всем: и мышкой, и клавиатурой, и скринридером.
Label и input
Каждое поле должно иметь label:
<label htmlFor="email">Email</label>
<input id="email" type="email" />
Или оборачивать:
<label>
Email
<input type="email" />
</label>
Клик по label фокусирует поле. Скринридер озвучивает название.
useId для уникальных ID
import { useId } from 'react';
function Input({ label }: { label: string }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
aria-invalid
Если поле с ошибкой — сообщай об этом:
<input
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error">{errors.email}</p>}
Обязательные поля
<label htmlFor="email">
Email <span aria-hidden="true">*</span>
</label>
<input id="email" required aria-required="true" />
aria-label
Если визуального label нет — используй aria-label:
<input type="search" aria-label="Поиск по сайту" />
Группировка
Связанные поля группируй в fieldset с legend:
<fieldset>
<legend>Способ оплаты</legend>
<label>
<input type="radio" name="payment" value="card" />
Карта
</label>
<label>
<input type="radio" name="payment" value="cash" />
Наличные
</label>
</fieldset>
Порядок табуляции
Поля должны быть в логическом порядке. Избегай tabIndex с положительными значениями.
Фокус
При ошибке фокусируй на первое проблемное поле:
const firstErrorRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (Object.keys(errors).length > 0) {
firstErrorRef.current?.focus();
}
}, [errors]);
Видимый фокус
Не убирай outline:
/* Плохо */
input:focus { outline: none; }
/* Хорошо */
input:focus-visible { outline: 2px solid var(--accent); }
:focus-visible показывает фокус только при навигации с клавиатуры.
Валидация и озвучивание
<input
aria-invalid={!!errors.email}
aria-describedby="email-error"
/>
<p id="email-error" role="alert">
{errors.email}
</p>
role="alert" заставляет скринридер озвучить сообщение.
Индикатор загрузки
<button disabled={isSubmitting} aria-busy={isSubmitting}>
{isSubmitting ? 'Отправка...' : 'Отправить'}
</button>
Автозаполнение
<input autoComplete="email" type="email" />
<input autoComplete="current-password" type="password" />
<input autoComplete="name" />
Облегчает заполнение.
Disabled vs readOnly
disabled— поле отключено, не отправляется, не фокусируетсяreadOnly— значение видно и отправляется, но менять нельзя
Клавиатура
Проверь, что форма работает без мыши: Tab, Shift+Tab, Enter, Space, стрелки.
Размер области нажатия
Минимум 44×44 px — для мобильных и людей с ограниченными возможностями.
Контраст
Текст и границы должны иметь достаточный контраст (WCAG AA — 4.5:1 для текста).
Итоги
- Всегда
labelдля полей useIdдля уникальных IDaria-invalid,aria-describedbyдля ошибокfieldsetдля групп- Логичный порядок табуляции
- Видимый фокус
role="alert"для сообщений- Проверяй на клавиатуре