Анатомия React компонента
Моё видение того, как должен выглядеть React компонент
421
7 мин.
Стандарт SSCF определяет требования к семантике, доступности и логике работы контактных форм.
<form>. Он предоставляет браузеру семантику формы, поведение клавиатуры и механизм отправки.<fieldset> и <legend>. <fieldset> применяется только там, где поля образуют понятную логическую группу; оборачивать им каждое поле по отдельности не требуется.<legend> задаёт название группы и располагается первым дочерним элементом <fieldset>.POST у <form> явно задаются method="post" и адрес обработчика через action. Для форм с файлами дополнительно задаётся enctype="multipart/form-data".id и осмысленный name. id связывает поле с подписью, а name задаёт имя поля в отправляемом наборе данных.<input> указывается подходящий type: например, text, email, tel, checkbox, file, number или range. У <select> и <textarea> атрибута type нет; тип взаимодействия определяется самим элементом.required добавляется только к полям, обязательность которых действительно обусловлена сценарием. Для необязательных полей он не используется.autocomplete задаётся подходящими токенами, например name, email, tel, given-name или family-name. Это помогает браузеру подобрать сохранённые данные и указывает назначение поля.placeholder необязателен. Он может показывать короткий пример формата, но не заменяет постоянную подпись <label> и не используется как единственное объяснение поля.<label> связывается с полем через одинаковые значения for и id либо оборачивает поле. Нажатие на подпись переводит фокус на соответствующее поле.Пример семантической структуры формы:
<form action="/ru/contact" method="post" enctype="multipart/form-data" data-contact-form>
<fieldset>
<legend>Контактные данные</legend>
<div class="form-field">
<label for="name">Имя</label>
<input
id="name"
name="name"
type="text"
autocomplete="name"
placeholder="Например, Анна"
required
/>
</div>
<div class="form-field">
<label for="email">Электронная почта</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
placeholder="name@example.com"
required
/>
</div>
<div class="form-field">
<label for="message">Сообщение</label>
<textarea
id="message"
name="message"
placeholder="Кратко опишите вопрос"
required
></textarea>
</div>
</fieldset>
<div class="form-consent">
<label class="form-consent__label">
<input id="data-consent" name="dataConsent" type="checkbox" required />
<span>Согласие на обработку данных</span>
</label>
<a href="/ru/policy" target="_blank" rel="noopener noreferrer">
Условия обработки данных (откроются в новой вкладке)
</a>
</div>
<button class="form-submit" type="submit">Отправить</button>
<p role="status" aria-live="polite" data-form-status></p>
</form>Чекбокс и его подпись объединены внутри <label>, поэтому нажатие на текст также переключает поле. Элемент <a> намеренно расположен вне <label>: ссылка остаётся отдельным интерактивным элементом. Открытие внутренней страницы в новой вкладке применяется только при обоснованной необходимости; в таком случае поведение заранее обозначается. Для target="_blank" задаётся rel="noopener noreferrer".
aria-invalid="true".aria-describedby. В сообщении указываются проблема и способ её исправления, например требуемый формат email.aria-invalid="true" не выставляется до того, как ошибка действительно обнаружена. После исправления значения состояние обновляется или снимается.Пример поля с сообщением об ошибке:
<div class="form-field form-field--error">
<label for="email">Электронная почта</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<p id="email-error">Укажите адрес электронной почты в корректном формате.</p>
</div>required, type="email", min, max и pattern, применяются там, где подходят сценарию. Пользовательская проверка дополняет их только необходимыми правилами.300 ms допустим для дорогой или сетевой проверки после паузы ввода; результат такой проверки не заменяет немедленную проверку при blur и перед отправкой.disabled не заменяет сообщения об ошибках.16px, чтобы избежать автоматического масштабирования страницы при фокусе.autocomplete, например name, email и tel.:autofill и :-webkit-autofill задаются читаемые цвета текста и фона с достаточным контрастом. Системный стиль не скрывается ценой неразличимого содержимого или фокуса.Базовые стили полей и состояния автозаполнения:
.form-field input,
.form-field select,
.form-field textarea,
.phone-field input {
font-size: 16px;
}
.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible,
.phone-field input:focus-visible {
outline: 2px solid #2457d6;
outline-offset: 2px;
}
.form-field [aria-invalid="true"] {
border-color: #b42318;
}
.form-field input:autofill,
.form-field input:-webkit-autofill,
.phone-field input:autofill,
.phone-field input:-webkit-autofill {
-webkit-text-fill-color: #161616;
box-shadow: 0 0 0 1000px #fff inset;
}Цвета в примере заменяются токенами интерфейса с проверенным контрастом. Размер 16px важен для текста самого поля; размер подписи или placeholder не меняет размер текста ввода.
type="tel" с autocomplete="tel". inputmode="tel" может дополнительно подсказать мобильному устройству подходящую экранную клавиатуру.+ и цифры кода страны не блокируются как универсально запрещённый ручной ввод: международный номер можно набрать или вставить. Если поле закрепляет код выбранной страны, повторный код нормализуется, а не добавляется к номеру второй раз.autofocus при загрузке формы. Для России национальный префикс 8 и международный +7 не считаются двумя одинаковыми масками: ввод нормализуется к выбранному формату.В React-проекте вместо @react-input/mask для международного телефонного поля используется react-international-phone. Переданный пример useMask({ mask, replacement }) относится к @react-input/mask и не является API телефонного пакета. Здесь используется PhoneInput, который форматирует номер, поддерживает выбор страны и обработку позиции курсора. Определение страны по IP не является встроенной функцией пакета, проверкой номера или заменой пользовательского выбора.
import { useState } from 'react';
import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';
export const PhoneField = () => {
const [phone, setPhone] = useState('');
return (
<div className="phone-field">
<label htmlFor="phone">Телефон</label>
<PhoneInput
defaultCountry="ru"
forceDialCode
value={phone}
onChange={(phone) => setPhone(phone)}
inputProps={{
id: 'phone',
name: 'phone',
type: 'tel',
autoComplete: 'tel',
inputMode: 'tel',
required: true,
}}
/>
</div>
);
};react-international-phone отвечает за ввод и форматирование, но актуальная документация пакета больше не рассматривает его как библиотеку валидации номера. Для проверки по планам нумерации применяется специализированная библиотека, например google-libphonenumber, а также серверная проверка.
import { PhoneNumberUtil } from 'google-libphonenumber';
const phoneUtil = PhoneNumberUtil.getInstance();
const isValidPhone = (phone) => {
try {
return phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(phone));
} catch (error) {
return false;
}
};Проверка формата и плана нумерации не подтверждает, что номер активен или принадлежит отправителю.
<select>. Он предоставляет встроенную клавиатурную и платформенную поддержку.<select>. Для него реализуются доступное имя, состояние раскрытия и выбора, управление клавиатурой и предсказуемое управление фокусом по подходящему ARIA-паттерну.<textarea> сохраняется возможность изменения размера, если она не нарушает макет. Если вертикальное изменение размера требуется ограничить, используется resize: vertical или иной обоснованный предел; безусловный resize: none может мешать вводу длинного текста.accept только подсказывает варианты файлового диалога и не является проверкой безопасности.<input type="range">, если его поведение соответствует задаче. Он уже поддерживает семантику, фокус, клавиатуру и изменение значения указателем.type="number". Диапазон, шаг и текущее значение синхронизируются из одного источника состояния, чтобы контролы не расходились.min, max, step и начальное value. Текущее значение показывается текстом, если без него положение ползунка недостаточно понятно.cursor: pointer может указывать на интерактивность ползунка. cursor: grab и cursor: grabbing применяются только когда контрол действительно реализует перетаскивание; курсор не заменяет поддержку касания и клавиатуры.Пример связанного числового поля и ползунка:
<div class="form-field">
<label for="budget-range">Бюджет</label>
<input
id="budget-range"
type="range"
min="0"
max="100000"
step="5000"
value="50000"
aria-describedby="budget-current-value"
/>
<output id="budget-current-value" for="budget-range">50 000 ₽</output>
<label for="budget-value">Точное значение, ₽</label>
<input
id="budget-value"
name="budget"
type="number"
min="0"
max="100000"
step="5000"
value="50000"
inputmode="numeric"
required
/>
</div>Оба контрола обновляют одно значение в состоянии интерфейса. В примере только числовое поле имеет name, поэтому значение отправляется один раз.
Пример синхронизации на нативных полях:
const rangeInput = document.querySelector('#budget-range');
const numberInput = document.querySelector('#budget-value');
const budgetOutput = document.querySelector('#budget-current-value');
const formatBudget = (value) => `${new Intl.NumberFormat('ru-RU').format(Number(value))} ₽`;
const updateBudgetOutput = () => {
budgetOutput.value = formatBudget(rangeInput.value);
};
rangeInput.addEventListener('input', () => {
numberInput.value = rangeInput.value;
updateBudgetOutput();
});
numberInput.addEventListener('change', () => {
if (!numberInput.validity.valid) return;
rangeInput.value = numberInput.value;
numberInput.value = rangeInput.value;
updateBudgetOutput();
});Ползунок обновляет числовое поле во время перемещения. Введённое число синхронизируется с ползунком после завершения ввода и только при прохождении ограничений min, max и step.
input[type="range"] {
cursor: pointer;
}
input[type="range"]:active {
cursor: grabbing;
}Tab и Shift+Tab; видимый фокус не удаляется.<select> управляется клавиатурой по правилам операционной системы и браузера. Пользовательский список должен самостоятельно реализовывать согласованное поведение клавиш, включая перемещение между вариантами и выбор.Space, когда он в фокусе. Нативный элемент сохраняет правильное состояние и доступен без добавления дублирующих ARIA-атрибутов.<label>. aria-label или aria-labelledby используются только тогда, когда видимой подписи нет или её недостаточно.aria-label или aria-labelledby.<label>, чтобы вся подпись переключала состояние. Ссылка на политику размещается отдельно от <label>, чтобы не вкладывать интерактивную ссылку в подпись.role="status" с aria-live="polite".disabled, а состояние формы — aria-busy="true".role="status". Изменения текста внутри заблокированной кнопки не всегда дают надёжное уведомление вспомогательным технологиям.Если кнопка действительно отключена, её визуальное состояние должно отличаться от доступного. Курсор и прозрачность — только визуальные подсказки; причина недоступности остаётся текстово понятной.
.form-submit:disabled {
cursor: not-allowed;
opacity: 0.6;
}disabled или ограничениям из интерфейса.POST по HTTPS. Применимость CSRF-защиты, ограничения частоты, идемпотентности и других мер определяется моделью угроз и способом аутентификации обработчика.Promise.allSettled() сообщает результат нескольких независимых обещаний, но не делает их атомарной транзакцией и не отменяет уже выполненные действия при частичном сбое. Сервер определяет, какой результат считается принятым, как повторяются неудачные доставки и как предотвращаются дубликаты.Пример отправки формы на серверный обработчик:
const form = document.querySelector('[data-contact-form]');
const submitButton = form.querySelector('[type="submit"]');
const formStatus = form.querySelector('[data-form-status]');
let isSubmitting = false;
form.addEventListener('submit', (event) => {
event.preventDefault();
if (isSubmitting || !form.reportValidity()) return;
isSubmitting = true;
submitButton.disabled = true;
form.setAttribute('aria-busy', 'true');
formStatus.textContent = 'Отправка…';
fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' },
})
.then((response) => {
if (!response.ok) throw new Error('Form submission failed');
form.reset();
formStatus.textContent = 'Форма успешно отправлена';
})
.catch(() => {
formStatus.textContent = 'Не удалось отправить форму. Проверьте соединение и повторите попытку.';
})
.then(() => {
isSubmitting = false;
submitButton.disabled = false;
form.removeAttribute('aria-busy');
});
});Пример предполагает наличие data-contact-form, кнопки отправки и области статуса в разметке. В реальном обработчике отдельно разбираются серверные ошибки, тайм-ауты и повторные запросы; клиентский код не обращается напрямую к внешним интеграциям.
<form>, группы <fieldset> и <legend> используются только для связанных полей.id, подходящий type там, где это применимо, и name, если значение должно быть отправлено.required, форматы полей, поведение autocomplete и различие между подписью и необязательным placeholder.Похожие категории:
Моё видение того, как должен выглядеть React компонент
421
7 мин.
Стандарт SST-A определяет требования к семантике, доступности и логике работы табов
62
6 мин.
Стандарт SSF-U определяет требования к семантике, доступности и логике работы фуллскрина
248
2 мин.
Стандарт SSA определяет требования к семантике, доступности и логике работы аккордеона
258
2 мин.
Разбор критических ошибок веб-дизайна. Почему слайдеры, автоплей и тяжёлые страницы снижают конверсию и позиции в Google и Yandex
232
1 мин.
Стандарт SSP определяет требования к семантике, доступности и логике работы пагинации
240
1 мин.