Валидация форм

HTML5 предоставляет встроенные атрибуты валидации (required, pattern, min/max), а Constraint Validation API позволяет создавать кастомные проверки через JavaScript.

Зачем нужно

Валидация на клиенте -- первая линия защиты от неправильных данных. Она даёт мгновенную обратную связь пользователю без обращения к серверу. Но серверная валидация обязательна -- клиентскую можно обойти.

Где используется

  • Все формы с обязательными полями
  • Регистрация/логин
  • Оформление заказа
  • Любой пользовательский ввод

Предпосылки

HTML-атрибуты валидации

required -- обязательное поле

<label for="name">Имя (обязательно):</label>
<input type="text" id="name" name="name" required>
<!-- Форма не отправится, если поле пустое -->

minlength / maxlength -- длина текста

<label for="pwd">Пароль (8-64 символов):</label>
<input type="password" id="pwd" name="password"
       required minlength="8" maxlength="64">

<label for="bio">О себе (до 200 символов):</label>
<textarea id="bio" name="bio" maxlength="200"></textarea>

min / max -- диапазон чисел/дат

<!-- Число -->
<label for="age">Возраст (18-120):</label>
<input type="number" id="age" name="age" min="18" max="120">

<!-- Дата -->
<label for="date">Дата (не раньше сегодня):</label>
<input type="date" id="date" name="date" min="2026-04-06">

step -- шаг

<!-- Целые числа -->
<input type="number" name="qty" min="1" max="100" step="1">

<!-- С десятичными (шаг 0.01) -->
<input type="number" name="price" min="0" step="0.01">

<!-- Любое число -->
<input type="number" name="value" step="any">

pattern -- регулярное выражение

<!-- Только буквы (кириллица + латиница) -->
<label for="name">Имя:</label>
<input type="text" id="name" name="name"
       pattern="[A-Za-zА-Яа-яЁё\s]+"
       title="Только буквы и пробелы">

<!-- Телефон -->
<input type="tel" name="phone"
       pattern="\+7\s?\(?\d{3}\)?\s?\d{3}[-\s]?\d{2}[-\s]?\d{2}"
       title="Формат: +7 (XXX) XXX-XX-XX">

<!-- Почтовый индекс -->
<input type="text" name="zip"
       pattern="\d{6}"
       title="6 цифр">

Атрибут title показывается в подсказке при ошибке валидации.

type -- встроенная валидация

<!-- Email: проверяет формат xxx@xxx -->
<input type="email" name="email" required>

<!-- URL: проверяет формат http(s)://... -->
<input type="url" name="website" required>

<!-- Number: принимает только числа -->
<input type="number" name="count" required>

CSS-псевдоклассы валидации

<style>
  /* Валидное поле */
  input:valid {
    border-color: green;
  }

  /* Невалидное поле */
  input:invalid {
    border-color: red;
  }

  /* Невалидное только после взаимодействия */
  input:user-invalid {
    border-color: red;
    background: #fff5f5;
  }

  /* В диапазоне / вне диапазона */
  input:in-range { border-color: green; }
  input:out-of-range { border-color: red; }

  /* Required пустое */
  input:placeholder-shown:required {
    border-color: orange;
  }

  /* Optional */
  input:optional {
    border-color: #ccc;
  }
</style>

<input type="email" name="email" required placeholder="Email">
<input type="number" name="age" min="18" max="99">

:user-invalid vs :invalid

:invalid применяется сразу (даже до взаимодействия), а :user-invalid -- только после того, как пользователь попытался ввести данные. Используй :user-invalid для лучшего UX.

novalidate -- отключение встроенной валидации

<!-- Отключить валидацию для всей формы -->
<form action="/api" method="POST" novalidate>
  <input type="email" name="email" required>
  <button type="submit">Отправить</button>
</form>

<!-- Отключить для конкретной кнопки -->
<form action="/api" method="POST">
  <input type="email" name="email" required>
  <button type="submit">Отправить</button>
  <button type="submit" formnovalidate>Сохранить черновик</button>
</form>

novalidate полезен когда ты реализуешь кастомную валидацию через JavaScript.

Constraint Validation API

Проверка валидности

<form id="myForm" novalidate>
  <label for="email">Email:</label>
  <input type="email" id="email" name="email" required>
  <span class="error" aria-live="polite"></span>

  <button type="submit">Отправить</button>
</form>

<script>
  const form = document.getElementById('myForm');
  const email = document.getElementById('email');
  const error = email.nextElementSibling;

  email.addEventListener('input', () => {
    if (email.validity.valid) {
      error.textContent = '';
    }
  });

  form.addEventListener('submit', (e) => {
    if (!email.validity.valid) {
      e.preventDefault();
      showError;
    }
  });

  function showError() {
    if (email.validity.valueMissing) {
      error.textContent = 'Email обязателен';
    } else if (email.validity.typeMismatch) {
      error.textContent = 'Введите корректный email';
    } else if (email.validity.patternMismatch) {
      error.textContent = 'Формат не соответствует';
    }
  }
</script>

Объект validity

const input = document.querySelector('input');

input.validity.valid;          // true если всё ОК
input.validity.valueMissing;   // required и пусто
input.validity.typeMismatch;   // тип не совпадает (email, url)
input.validity.patternMismatch;// pattern не совпал
input.validity.tooLong;        // длиннее maxlength
input.validity.tooShort;       // короче minlength
input.validity.rangeOverflow;  // больше max
input.validity.rangeUnderflow; // меньше min
input.validity.stepMismatch;   // не кратно step
input.validity.badInput;       // невалидный ввод (буквы в number)
input.validity.customError;    // установлена кастомная ошибка

setCustomValidity -- кастомная ошибка

<form>
  <label for="pwd">Пароль:</label>
  <input type="password" id="pwd" name="password" required minlength="8">

  <label for="pwd2">Подтверждение:</label>
  <input type="password" id="pwd2" name="password_confirm" required>

  <button type="submit">Регистрация</button>
</form>

<script>
  const pwd = document.getElementById('pwd');
  const pwd2 = document.getElementById('pwd2');

  pwd2.addEventListener('input', () => {
    if (pwd.value !== pwd2.value) {
      pwd2.setCustomValidity('Пароли не совпадают');
    } else {
      pwd2.setCustomValidity(''); // Сброс ошибки
    }
  });
</script>

reportValidity и checkValidity

// checkValidity -- проверяет, генерирует событие invalid
const isValid = form.checkValidity; // true/false

// reportValidity -- проверяет И показывает нативные тултипы ошибок
form.reportValidity;

// Для одного поля
input.checkValidity;
input.reportValidity;

Полный пример: форма с кастомной валидацией

<form id="registerForm" novalidate>
  <div class="field">
    <label for="reg-email">Email:</label>
    <input type="email" id="reg-email" name="email" required
           aria-describedby="email-error">
    <p class="error" id="email-error" role="alert"></p>
  </div>

  <div class="field">
    <label for="reg-pwd">Пароль:</label>
    <input type="password" id="reg-pwd" name="password"
           required minlength="8"
           aria-describedby="pwd-hint pwd-error">
    <p class="hint" id="pwd-hint">Минимум 8 символов</p>
    <p class="error" id="pwd-error" role="alert"></p>
  </div>

  <button type="submit">Зарегистрироваться</button>
</form>

<style>
  .error { color: #d32f2f; font-size: 0.875rem; min-height: 1.25rem; }
  .hint { color: #666; font-size: 0.875rem; }
  .field { margin-bottom: 1rem; }
  input:user-invalid { border-color: #d32f2f; }
  input:user-valid { border-color: #388e3c; }
</style>

<script>
  const form = document.getElementById('registerForm');

  form.addEventListener('submit', (e) => {
    // Очистить ошибки
    form.querySelectorAll('.error').forEach(el => el.textContent = '');

    let isValid = true;

    form.querySelectorAll('input').forEach(input => {
      if (!input.validity.valid) {
        isValid = false;
        const errorEl = document.getElementById(input.id.replace('reg-', '') + '-error');
        if (errorEl) {
          errorEl.textContent = input.validationMessage || 'Ошибка';
        }
      }
    });

    if (!isValid) {
      e.preventDefault();
      // Фокус на первое невалидное поле
      form.querySelector('input:invalid')?.focus();
    }
  });
</script>

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

Ошибка Почему плохо Как правильно
Только клиентская валидация Легко обойти через DevTools Всегда валидируй на сервере
:invalid вместо :user-invalid Поля подсвечены ошибкой до ввода :user-invalid или JS-валидация
Нет title с pattern Непонятное сообщение об ошибке title объясняет формат
Ошибки без role="alert" Screen reader не объявит ошибку role="alert" или aria-live
Нет фокуса на ошибке Пользователь не видит проблему Фокус на первое невалидное поле
setCustomValidity без сброса Поле остаётся невалидным навсегда setCustomValidity('') при исправлении

Практика

  1. Создай форму с required, minlength, pattern и проверь встроенные сообщения об ошибках
  2. Стилизуй валидные/невалидные поля через :valid/:invalid
  3. Реализуй проверку совпадения паролей через setCustomValidity
  4. Создай кастомную валидацию с novalidate и Constraint Validation API
  5. Добавь aria-describedby для связи ошибок с полями

Связанные темы

Ресурсы