Валидация форм: как совместить Constraint API и свою логику
Браузер умеет проверять поля без единой строки JavaScript, но по умолчанию делает это некрасиво и не так, как нужно проекту. Разбираем, где заканчивается встроенная валидация и как аккуратно достроить свою.

Типичная ситуация: форма регистрации проверяет email через required и type="email", но пользователь всё равно отправляет пустой пароль и получает 400 с сервера. Причина в том, что встроенная валидация HTML и ручные проверки на JavaScript живут отдельно и мешают друг другу: кто-то вешает onsubmit, забывает про novalidate, и в итоге срабатывают обе проверки с разными сообщениями. Разберём, как заставить их работать как одну систему — на Constraint Validation API, который поддерживают все актуальные браузеры.
Речь именно об угле «как правильно совместить»: не про кастомные UI-библиотеки форм и не про серверную валидацию (она обязательна в любом случае и здесь только упоминается). Только про связку нативных ограничений, Constraint Validation API и CSS-состояний.
Три уровня валидации, которые путают
В браузере одновременно доступны три механизма, и ошибки начинаются, когда их смешивают в кучу:
- HTML-атрибуты —
required,min,max,minlength,maxlength,pattern,type. Браузер проверяет их сам и блокирует отправку. - Constraint Validation API — JavaScript-интерфейс поверх тех же ограничений:
element.validity,element.checkValidity(),element.setCustomValidity(). Позволяет читать состояние и добавлять свои правила. - CSS-псевдоклассы —
:valid,:invalid,:user-invalid,:required. Реагируют на то же состояние без единой строки JS.
Главная ошибка новичка — писать ручную проверку регуляркой в onsubmit и параллельно оставлять required на поле. Тогда браузер сначала покажет свой пузырь «Заполните это поле», а ваш код до проверки даже не дойдёт.
Начните с HTML, а не с JavaScript
Максимум ограничений вынесите в разметку. Это работает без JavaScript, доступно скринридерам и не ломается, если ваш скрипт не загрузился.
<form id="signup">
<label>
Email
<input type="email" name="email" required
autocomplete="email">
</label>
<label>
Пароль
<input type="password" name="password"
required minlength="8"
autocomplete="new-password">
</label>
<button type="submit">Зарегистрироваться</button>
</form>Атрибут type="email" проверяет наличие @ и домена, но не строгий формат — браузер намеренно нестрогий, чтобы не отсекать редкие валидные адреса. Не пытайтесь заменить его сложной регуляркой в pattern: реальная проверка email возможна только отправкой письма.
pattern для форматов, которых нет среди типов
Для телефона, ИНН или почтового индекса используйте pattern — это регулярка без якорей ^ и $, браузер проверяет всю строку целиком:
<input name="index" pattern="[0-9]{6}"
title="Шесть цифр" required>Атрибут title здесь не просто подсказка при наведении: браузер добавит его текст в сообщение об ошибке.
Кастомные правила через setCustomValidity
Есть проверки, которые HTML не выражает: совпадение двух паролей, зависимость одного поля от другого. Не делайте это отдельным if в обработчике — встройте в ту же систему через setCustomValidity.
const form = document.getElementById('signup');
const password = form.password;
const confirm = form.querySelector('[name="confirm"]');
// Проверяем совпадение при каждом вводе в поле подтверждения
confirm.addEventListener('input', () => {
if (confirm.value !== password.value) {
confirm.setCustomValidity('Пароли не совпадают');
} else {
// Пустая строка снимает ошибку — обязательно сбрасывать
confirm.setCustomValidity('');
}
});Ключевой момент: пока в setCustomValidity непустая строка, поле считается невалидным, а form.checkValidity() вернёт false. Забыли сбросить пустой строкой — поле навсегда останется невалидным.
Не подменяйте нативную валидацию своей — надстраивайте её. HTML-ограничения иsetCustomValidityпитают один и тот же объектvalidity, поэтому браузер, CSS и ваш JavaScript видят одно состояние.
Отключаем нативные пузыри, но не проверку
Дефолтные всплывающие пузыри выглядят по-разному в каждом браузере и не поддаются стилизации. Обычный приём — отключить их отображение через novalidate на форме, но сохранить проверку через API:
form.addEventListener('submit', (event) => {
// Проверяем всю форму разом
if (!form.checkValidity()) {
event.preventDefault();
// Показываем свои сообщения
for (const field of form.elements) {
if (field.willValidate && !field.checkValidity()) {
showError(field, field.validationMessage);
}
}
return;
}
// Форма валидна — отправка идёт дальше
});
function showError(field, message) {
const box = field.closest('label').querySelector('.error')
|| createErrorBox(field);
box.textContent = message;
}Атрибут novalidate ставится на сам тег <form novalidate>. Он отключает только автоматические пузыри и блокировку отправки — методы checkValidity() и объект validity продолжают работать. Свойство validationMessage отдаёт локализованный текст ошибки, который браузер показал бы сам.
CSS: не подсвечивайте поле красным до ввода
Самая частая визуальная ошибка — стилизовать :invalid, из-за чего все обязательные поля становятся красными сразу при загрузке страницы, ещё до того как пользователь к ним прикоснулся.
Для этого есть :user-invalid — псевдокласс, который срабатывает только после того, как пользователь взаимодействовал с полем и ушёл из него (или попытался отправить форму):
/* Плохо: красное сразу при загрузке */
input:invalid {
border-color: red;
}
/* Хорошо: только после взаимодействия */
input:user-invalid {
border-color: #d33;
}
input:user-valid {
border-color: #2a2;
}Псевдоклассы :user-valid и :user-invalid поддерживаются в актуальных версиях Chrome, Firefox и Safari. Если нужна поддержка старых браузеров, эмулируйте поведение классом, который вешаете на поле по событию blur, — проверьте текущую совместимость на MDN или caniuse перед выбором.
Сравнение подходов к моменту проверки
| Когда проверять | Плюс | Минус |
|---|---|---|
На submit | Не дёргает пользователя во время ввода | Все ошибки вываливаются разом в конце |
На blur (уход из поля) | Ошибка появляется по завершении поля | Не отреагирует, если поле не трогали |
На input (каждый символ) | Мгновенная обратная связь | Раздражает: «невалидно» на первом же символе email |
Рабочая комбинация: первую проверку показывать на blur или submit, а вот снятие уже показанной ошибки — на input. Тогда пользователь видит, что исправление засчитано сразу, но не получает красноту, пока пишет.
Что ломается на практике
- Забытый сброс
setCustomValidity('')— поле остаётся невалидным навсегда, форма не отправляется без видимой причины. - Стилизация
:invalidвместо:user-invalid— вся форма красная при загрузке. - Ручная проверка поверх нативной без
novalidate— два конфликтующих сообщения, пузырь браузера перекрывает ваш текст. - Отсутствие серверной валидации — клиентская проверка отключается в DevTools за секунду. Всё, что критично, проверяйте на сервере повторно.
- Динамически добавленные поля — если вы вставляете
<input>скриптом, обработчикиinputна них не навесятся сами; используйте делегирование событий на форме.
Что делать дальше
Соберите форму по порядку: сначала максимум ограничений в HTML, затем novalidate и один обработчик submit с checkValidity(), потом кастомные правила через setCustomValidity, и в конце — CSS на :user-invalid. Проверьте с выключенным JavaScript: браузер должен отвалидировать форму сам. Потом продублируйте все правила на сервере — клиентская валидация нужна для удобства, а не для безопасности. Дальше стоит посмотреть в сторону доступности: связать сообщение об ошибке с полем через aria-describedby и aria-invalid, чтобы скринридер зачитывал ошибку.
Субтитры и таймкоды: Расшифровка речи и субтитры по правилам читаемости: 42 знака в строке, две строки, минимум секунда на реплику.
Спросить за 40 ₽Источники: Client-side form validation — MDN Web Docs, Constraint Validation — MDN Web Docs
Частые вопросы
Нужна ли серверная валидация, если клиентская уже работает?
Что делать, если браузер пользователя не поддерживает :user-invalid?
Форма не отправляется, но ошибок не видно — в чём дело?
Можно ли проверять email строгой регуляркой в pattern?
Как показать ошибку рядом с полем, а не всплывающим пузырём браузера?
Работает ли валидация на динамически добавленных полях?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.