Форма это самый честный момент разговора с продуктом. До неё человек листал, читал, сравнивал, а теперь от него требуют данные. И именно здесь люди уходят чаще всего: посреди заполнения, молча, без объяснений. Менеджер видит в воронке провал между "начал заполнять" и "отправил", но причин провала в цифрах не видно.

Причины прячутся в мелочах. Поле кричит об ошибке до первого касания, сообщение про "неверный формат" не объясняет, какой формат верный, кнопка говорит "ОК" вместо понятного действия, форма просит телефон, а зачем он нужен, не поясняет. Валидация, которая наказывает вместо того, чтобы помогать, превращает заполняющего в ушедшего. Ниже разбор проверки, которая удерживает: от атрибутов браузера и момента показа ошибки до текстов, автозаполнения и метрик по полям.

Где формы теряют людей и что чувствует заполняющий

Типовая сцена оттока выглядит так. Человек заполняет заявку на доставку из четырнадцати полей, нажимает отправку, страница перезагружается, и часть введённого стёрта, а красным подсвечено поле, о существовании требований которого он даже не подозревал. Второй попытки чаще всего не будет: если первая стоила десяти минут, вторая выглядит как плата за чужую небрежность.

Список болей повторяется от сайта к сайту: проверка срабатывает на каждый нажатый символ, красная рамка появляется до ввода, сообщение об ошибке не говорит, что делать, обязательность поля не помечена, сервер думает десять секунд, а потом просит "попробовать позже". Каждая мелочь по отдельности терпима, вместе они складываются в уверенность, что здесь что-то захотят продать или обмануть.

Форма живёт по принципу "не удивляй". Предсказуемое поведение, мгновенная реакция, уважение к уже введённому и честное объяснение, зачем нужны данные. Конверсия начинается не с красивой кнопки, а с ощущения, что заполнение под контролем у человека, а не у машины.

Нативная валидация браузера и атрибуты без единой строки скрипта

Половина валидации уже встроена в браузер и ждёт, пока её включат. Атрибут required делает поле обязательным, type со значениями email, url или tel включает проверку формата, minlength и maxlength ограничивают длину, pattern принимает регулярное выражение, min, max и step работают с числами и датами. Атрибут inputmode подсказывает мобильной клавиатуре нужный набор: numeric для кода, email для почты.

<label for="email">Рабочая почта</label>
<input id="email" name="email" type="email"
       required autocomplete="email" inputmode="email"
       aria-describedby="email-error">
<p id="email-error" class="field-error" hidden></p>

Скрипты подключаются через программный интерфейс проверки: метод checkValidity у поля и формы возвращает результат, setCustomValidity задаёт своё сообщение, а объект validity описывает конкретную причину, valueMissing, typeMismatch, tooShort, patternMismatch. Атрибут novalidate на форме отключает стандартные всплывашки браузера, чтобы интерфейс показывал ошибки по-своему, в едином стиле и в правильном месте.

Отдельная ловушка спрятана в стилях. Псевдокласс :invalid срабатывает и на пустом поле до первого касания, поэтому красная рамка встречает человека раньше, чем он начал печатать. В свежих браузерах есть :user-invalid, который оживает только после взаимодействия с полем, а на старых используют приём с :not(:placeholder-shown) и обязательным заполнителем.

input:user-invalid {
  border-color: #d93025;
}

Момент показа ошибки и правило первого касания поля

Главный вопрос UX-валидации звучит коротко: когда показывать ошибку. Проверять на каждый символ жестоко, проверять только на отправку слишком поздно, человек узнаёт о проблеме после того, как вложился. Рабочая схема из трёх тактов: молчаливое ожидание, проверка при потере фокуса, помощь по ходу набора после первой ошибки.

const form = document.querySelector("form[data-validate]");

form.addEventListener("focusout", (event) => {
  const field = event.target;
  if (!field.matches("input, select, textarea")) return;
  showFieldError(field, describeError(field));
});

form.addEventListener("input", (event) => {
  const field = event.target;
  if (field.dataset.invalid === "true") {
    showFieldError(field, describeError(field));
  }
});

form.addEventListener("submit", (event) => {
  const broken = [...form.elements].filter((el) => !el.checkValidity());
  if (broken.length > 0) {
    event.preventDefault();
    showSummary(broken);
    broken[0].focus();
  }
});

Логика простая. Пока поле в фокусе и человек печатает, интерфейс молчит: черновик имеет право на ошибки. Фокус ушёл, поле заполнено плохо, ошибка показывается рядом с полем, спокойно и без красной вспышки на весь экран. После первой ошибки проверка переходит в режим помощи: каждое нажатие обновляет состояние, ошибка исчезает сразу, как только текст стал корректным. На отправке форма проверяет себя целиком, собирает сводку проблем и переводит фокус на первое проблемное поле, чтобы человек не искал его глазами.

Текст ошибки который помогает а не стыдит

Стандартные фразы вроде "Неверно" или "Ошибка в поле" бесполезны: они сообщают факт, который человек и так видел по красной рамке. Полезное сообщение называет причину и способ исправления. Сравнение двух вариантов для поля почты: "Неверный формат" против "Проверьте адрес, похоже, пропущен знак @ после имени". Второй вариант не просто вежливее, он сокращает время заполнения, потому что подсказывает конкретное действие.

function describeError(field) {
  const state = field.validity;
  if (state.valueMissing) return "Заполните это поле";
  if (state.typeMismatch) return "Проверьте адрес, нужен знак @ и домен";
  if (state.tooShort) return "Добавьте ещё, минимум " + field.minLength;
  if (state.patternMismatch) return "Допустимы буквы, цифры и дефис";
  return "";
}

function showFieldError(field, message) {
  const box = document.getElementById(field.id + "-error");
  const hasError = message !== "";
  field.setAttribute("aria-invalid", String(hasError));
  field.dataset.invalid = String(hasError);
  box.hidden = !hasError;
  box.textContent = message;
}

Тон сообщений настраивается осознанно: без восклицаний, без слова "неверно" в лоб, без обвинений. Стандарты доступности формулируют два жёстких требования: ошибка обязана быть описана текстом, который читают программы экранного доступа, и там, где проблема распознаваема, интерфейс обязан предлагать исправление, а не просто констатировать провал. Технически это решается связкой полей с подсказками через aria-describedby, атрибутом aria-invalid у проблемного поля и живой областью с ролью alert для динамических сообщений, а сводка ошибок над формой с переходами к каждому полю закрывает случай, когда проблем несколько.

Отдельное правило про пароль: после неудачной отправки поле пароля очищать нельзя. Просить ввести секрет заново из-за ошибки в соседнем поле это верный способ получить брошенную форму и разозлённого человека.

Асинхронные проверки и живая обратная связь интерфейса

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

function debounce(fn, pause = 400) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), pause);
  };
}

const checkLogin = debounce(async (value) => {
  const answer = await fetch("/api/check-login?value=" + encodeURIComponent(value));
  const { free } = await answer.json();
  hintLogin(free);
});

Обратная связь обязана быть видимой: строка "проверяем" рядом с полем, результат словами, "логин свободен" или "этот логин уже занят, попробуйте другой". Молчаливая проверка выглядит как зависший интерфейс. И пара правил про кнопку: пока идёт асинхронная проверка поля, кнопку отправки блокировать не стоит, человек имеет право не ждать; после нажатия отправки кнопку отключают и показывают ход, иначе двойной клик рождает дубль заявки и письмо о двух заказах из одного. Ответ сервера, который занимает больше двух-трёх секунд, воспринимается как поломка, поэтому долгие операции уносят в фон с честным обещанием прислать результат письмом.

Автозаполнение менеджеры паролей и разбиение на шаги

Автозаполнение это союзник конверсии, а не угроза. Менеджеры паролей и браузеры заполняют формы мгновенно, но только когда поля размечены честно: атрибут autocomplete со значениями email, given-name, family-name, tel, organization, street-address, postal-code позволяет распознать назначение поля. Для кода подтверждения из сообщения работает значение one-time-code: на мобильном устройстве код подставляется одним касанием, и шесть маленьких квадратиков, в которые нельзя вставить скопированное, проигрывают одному обычному полю с правильной разметкой.

Правила про пароль заслуживают отдельного абзаца. Требования к паролю показывают до ввода, а не после ошибки: минимум символов, классы знаков, запрет на повтор прошлого пароля. Индикатор надёжности говорит словами, "слабый, так себе, надёжный", а не голым процентом. Просить пароль дважды вышло из моды: подтверждение по почте надёжнее и бесит меньше. И вишенка, за которую люди благодарят: вставка из буфера никогда не блокируется, потому что менеджер паролей вставляет длинный секрет именно так.

Длинные формы дробят на шаги: одна тема на экран, контакты отдельно от деталей доставки, прогресс-бар показывает, сколько осталось. Обязательное условие дробления: кнопка назад не теряет введённое, а черновик сохраняется локально, чтобы случайно закрытая вкладка не стирала десять минут работы. Лишние поля при этом безжалостно режутся: каждое необязательное поле либо уходит совсем, либо помечается словом "необязательно", потому что молчаливая обязательность всего подряд рождает ложные ошибки на пустых полях.

Серверная проверка и метрики по полям формы

Клиентская валидация существует для скорости и удобства, серверная для правды. Любую проверку на клиенте обходят за секунды, поэтому сервер проверяет всё заново: обязательность, форматы, длины, уникальность, права доступа. Критично поведение после провала: человек должен вернуться к заполненной форме, увидеть ошибку у конкретного поля и поправить её, а не начинать с белого листа. Сопоставление ошибок сервера с полями интерфейса строится по имени поля в ответе, и это честный контракт, который экономит и нервы, и конверсию.

Метрики формы измеряются не в целом, а по полям. Воронка "открыл, начал, дошёл до третьего поля, бросил" показывает конкретное место оттока, счётчики повторных отправок и злых многократных нажатий выдают поля, которые не подсказывают, что от них хотят. Проверка формы на пяти живых респондентах из целевой аудитории находит большинство грубых проблем быстрее любого опроса, а мобильный прогон с автозаполнением и клавиатурой ловит то, чего не видно на большом мониторе разработчика.

Перед запуском форму полезно прогнать по короткому чек-листу:

  1. Каждое поле подписано понятным текстом, а обязательность видна и в интерфейсе, и в разметке;
  2. Ошибки появляются после потери фокуса, повторная проверка идёт по ходу набора;
  3. Сообщение об ошибке называет конкретную причину и способ её устранить;
  4. Подсказки связаны с полями через aria-describedby, а сводка ошибок ведёт к проблемным местам;
  5. Автозаполнение включено через правильные значения атрибута autocomplete, вставка из буфера не блокируется;
  6. Кнопка отправки защищена от двойного нажатия, а значения полей сохраняются после неудачной попытки;
  7. Метрики фиксируют поле, на котором люди уходят, а не только факт отправки формы.

Форма редко бывает главным украшением продукта, но именно она закрывает сделку: подписку, заявку, доставку, оплату. Хорошая новость в том, что большинство правил из этого разбора стоят дёшево: несколько атрибутов, вдумчивые тексты ошибок и честное отношение к тому, что человек уже ввёл. Плохая новость в том, что без этих мелочей человек уходит к конкуренту, у которого поле почты просто подсвечивается вовремя.