Форма это самый честный момент разговора с продуктом. До неё человек листал, читал, сравнивал, а теперь от него требуют данные. И именно здесь люди уходят чаще всего: посреди заполнения, молча, без объяснений. Менеджер видит в воронке провал между "начал заполнять" и "отправил", но причин провала в цифрах не видно.
Причины прячутся в мелочах. Поле кричит об ошибке до первого касания, сообщение про "неверный формат" не объясняет, какой формат верный, кнопка говорит "ОК" вместо понятного действия, форма просит телефон, а зачем он нужен, не поясняет. Валидация, которая наказывает вместо того, чтобы помогать, превращает заполняющего в ушедшего. Ниже разбор проверки, которая удерживает: от атрибутов браузера и момента показа ошибки до текстов, автозаполнения и метрик по полям.
Где формы теряют людей и что чувствует заполняющий
Типовая сцена оттока выглядит так. Человек заполняет заявку на доставку из четырнадцати полей, нажимает отправку, страница перезагружается, и часть введённого стёрта, а красным подсвечено поле, о существовании требований которого он даже не подозревал. Второй попытки чаще всего не будет: если первая стоила десяти минут, вторая выглядит как плата за чужую небрежность.
Список болей повторяется от сайта к сайту: проверка срабатывает на каждый нажатый символ, красная рамка появляется до ввода, сообщение об ошибке не говорит, что делать, обязательность поля не помечена, сервер думает десять секунд, а потом просит "попробовать позже". Каждая мелочь по отдельности терпима, вместе они складываются в уверенность, что здесь что-то захотят продать или обмануть.
Форма живёт по принципу "не удивляй". Предсказуемое поведение, мгновенная реакция, уважение к уже введённому и честное объяснение, зачем нужны данные. Конверсия начинается не с красивой кнопки, а с ощущения, что заполнение под контролем у человека, а не у машины.
Нативная валидация браузера и атрибуты без единой строки скрипта
Половина валидации уже встроена в браузер и ждёт, пока её включат. Атрибут 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: на мобильном устройстве код подставляется одним касанием, и шесть маленьких квадратиков, в которые нельзя вставить скопированное, проигрывают одному обычному полю с правильной разметкой.
Правила про пароль заслуживают отдельного абзаца. Требования к паролю показывают до ввода, а не после ошибки: минимум символов, классы знаков, запрет на повтор прошлого пароля. Индикатор надёжности говорит словами, "слабый, так себе, надёжный", а не голым процентом. Просить пароль дважды вышло из моды: подтверждение по почте надёжнее и бесит меньше. И вишенка, за которую люди благодарят: вставка из буфера никогда не блокируется, потому что менеджер паролей вставляет длинный секрет именно так.
Длинные формы дробят на шаги: одна тема на экран, контакты отдельно от деталей доставки, прогресс-бар показывает, сколько осталось. Обязательное условие дробления: кнопка назад не теряет введённое, а черновик сохраняется локально, чтобы случайно закрытая вкладка не стирала десять минут работы. Лишние поля при этом безжалостно режутся: каждое необязательное поле либо уходит совсем, либо помечается словом "необязательно", потому что молчаливая обязательность всего подряд рождает ложные ошибки на пустых полях.
Серверная проверка и метрики по полям формы
Клиентская валидация существует для скорости и удобства, серверная для правды. Любую проверку на клиенте обходят за секунды, поэтому сервер проверяет всё заново: обязательность, форматы, длины, уникальность, права доступа. Критично поведение после провала: человек должен вернуться к заполненной форме, увидеть ошибку у конкретного поля и поправить её, а не начинать с белого листа. Сопоставление ошибок сервера с полями интерфейса строится по имени поля в ответе, и это честный контракт, который экономит и нервы, и конверсию.
Метрики формы измеряются не в целом, а по полям. Воронка "открыл, начал, дошёл до третьего поля, бросил" показывает конкретное место оттока, счётчики повторных отправок и злых многократных нажатий выдают поля, которые не подсказывают, что от них хотят. Проверка формы на пяти живых респондентах из целевой аудитории находит большинство грубых проблем быстрее любого опроса, а мобильный прогон с автозаполнением и клавиатурой ловит то, чего не видно на большом мониторе разработчика.
Перед запуском форму полезно прогнать по короткому чек-листу:
- Каждое поле подписано понятным текстом, а обязательность видна и в интерфейсе, и в разметке;
- Ошибки появляются после потери фокуса, повторная проверка идёт по ходу набора;
- Сообщение об ошибке называет конкретную причину и способ её устранить;
- Подсказки связаны с полями через aria-describedby, а сводка ошибок ведёт к проблемным местам;
- Автозаполнение включено через правильные значения атрибута autocomplete, вставка из буфера не блокируется;
- Кнопка отправки защищена от двойного нажатия, а значения полей сохраняются после неудачной попытки;
- Метрики фиксируют поле, на котором люди уходят, а не только факт отправки формы.
Форма редко бывает главным украшением продукта, но именно она закрывает сделку: подписку, заявку, доставку, оплату. Хорошая новость в том, что большинство правил из этого разбора стоят дёшево: несколько атрибутов, вдумчивые тексты ошибок и честное отношение к тому, что человек уже ввёл. Плохая новость в том, что без этих мелочей человек уходит к конкуренту, у которого поле почты просто подсвечивается вовремя.