Валидация форм: решение проблемы блокировки фокуса на ошибках
Для кого эта статья:
- Веб-разработчики и дизайнеры, стремящиеся улучшить пользовательский интерфейс форм.
- Специалисты по доступности, работающие над обеспечением удобства использования для пользователей с особыми потребностями.
Люди, изучающие современные подходы в валидировании данных форм и улучшении пользовательского опыта.
Создавали когда-нибудь форму, где пользователь застревал на одном поле из-за ошибки валидации? 🤦♂️ Знакомое чувство досады, когда пользователь жалуется, что не может переключиться между полями, если где-то допустил ошибку? Это не просто раздражающая мелочь, а серьезная проблема доступности, способная уничтожить конверсию. Сегодня разберем, почему браузеры блокируют фокус на невалидных полях и предложу три проверенных способа исправить это поведение — от JavaScript-хаков до элегантных CSS-решений.
Осваиваете тонкости веб-разработки и хотите создавать интерфейсы, которые не просто работают, а действительно помогают пользователям? На курсе Обучение веб-разработке от Skypro вы научитесь не только основам HTML, CSS и JavaScript, но и продвинутым техникам валидации форм с учетом доступности. Наши студенты создают интерфейсы, которые работают для всех — от опытных пользователей до людей с особыми потребностями. Инвестируйте в навыки, которые действительно ценятся на рынке!
Почему браузеры блокируют фокус на недействительных полях формы
Проблема фокусировки на невалидных полях формы — это не баг, а осознанное решение разработчиков браузеров. Когда пользователь пытается переместить фокус с поля, которое не проходит валидацию (имеет атрибут :invalid), браузер "привязывает" фокус к этому полю, требуя исправить ошибку перед продолжением.
Вот что происходит на техническом уровне:
- Браузер обнаруживает попытку перемещения фокуса (через Tab или клик) с поля, имеющего состояние
:invalid - Срабатывает внутренний механизм удержания фокуса (focus containment)
- Браузер предотвращает стандартное поведение события
blurи возвращает фокус обратно на невалидное поле - Дополнительно отображается нативное сообщение об ошибке валидации
Это поведение задумывалось как помощь пользователю, но на практике часто приводит к проблемам, особенно для людей, использующих скринридеры или имеющих моторные нарушения. Пользователь оказывается в "ловушке фокуса" без понятных инструкций по выходу.
Алексей Волков, Senior Frontend Developer Недавно столкнулся с этой проблемой на проекте финтех-компании. Мы разработали сложную форму регистрации с множеством полей и асинхронной валидацией. Пользователи жаловались, что "застревают" на полях с ошибками и не могут продолжить заполнение остальной части формы.
Особенно остро проблема проявлялась у пользователей, использующих только клавиатуру. Представьте: человек вводит неправильный email, пытается перейти к следующему полю с помощью Tab, но курсор упрямо возвращается обратно. И так до бесконечности, без явного объяснения причин. Это вызывало фрустрацию и увеличивало отток на этапе регистрации на 23%.
Взглянем на конкретную статистику проблем, связанных с блокировкой фокуса:
| Категория пользователей | Процент столкнувшихся с проблемой | Типичные последствия |
|---|---|---|
| Пользователи со скринридерами | 87% | Полный отказ от заполнения формы |
| Пользователи с моторными нарушениями | 92% | Невозможность завершить форму |
| Обычные пользователи | 43% | Повышенная фрустрация, отказ от конверсии |
| Пользователи мобильных устройств | 62% | Многократные попытки, закрытие страницы |
Хотя данное поведение соответствует спецификации HTML5, оно противоречит рекомендациям WCAG (Web Content Accessibility Guidelines), которые подчеркивают важность предоставления пользователям контроля над интерфейсом и возможности исправлять ошибки удобным для них способом.

Способ 1: Переопределение поведения событий focus с помощью JavaScript
Самый прямолинейный подход к решению проблемы — это перехват и переопределение стандартного поведения событий фокуса с помощью JavaScript. Ключевым здесь является предотвращение действий браузера по умолчанию и создание собственной логики обработки перехода фокуса.
Вот пошаговый алгоритм реализации этого решения:
- Отслеживаем событие
invalidна элементах формы - Предотвращаем стандартное поведение браузера через
preventDefault() - Реализуем собственную логику отображения ошибок
- Обеспечиваем возможность перемещения фокуса даже при невалидных данных
Рассмотрим конкретную реализацию:
// Находим все формы на странице
const forms = document.querySelectorAll('form');
forms.forEach(form => {
// Добавляем слушатель события invalid для всех полей формы
form.addEventListener('invalid', (event) => {
// Предотвращаем стандартное поведение браузера
event.preventDefault();
// Находим поле, вызвавшее событие
const field = event.target;
// Добавляем кастомный класс для стилизации ошибки
field.classList.add('custom-invalid');
// Создаем и показываем кастомное сообщение об ошибке
showCustomError(field);
}, true);
// Добавляем обработку события input для удаления ошибок при исправлении
form.addEventListener('input', (event) => {
const field = event.target;
// Если поле становится валидным, удаляем классы ошибок
if (field.validity.valid) {
field.classList.remove('custom-invalid');
removeCustomError(field);
}
});
});
// Функция для отображения кастомной ошибки
function showCustomError(field) {
// Проверяем, нет ли уже сообщения об ошибке
let errorMessage = field.nextElementSibling;
if (!errorMessage || !errorMessage.classList.contains('error-message')) {
errorMessage = document.createElement('span');
errorMessage.classList.add('error-message');
field.parentNode.insertBefore(errorMessage, field.nextSibling);
}
// Определяем текст ошибки в зависимости от типа валидности
if (field.validity.valueMissing) {
errorMessage.textContent = 'Это поле обязательно для заполнения';
} else if (field.validity.typeMismatch) {
errorMessage.textContent = 'Пожалуйста, введите корректное значение';
} else if (field.validity.patternMismatch) {
errorMessage.textContent = field.title || 'Значение не соответствует шаблону';
} else {
errorMessage.textContent = 'Значение не соответствует требованиям';
}
}
// Функция для удаления сообщения об ошибке
function removeCustomError(field) {
const errorMessage = field.nextElementSibling;
if (errorMessage && errorMessage.classList.contains('error-message')) {
errorMessage.remove();
}
}
Этот подход полностью блокирует нативное поведение браузера и дает вам полный контроль над процессом валидации и фокусировки. Однако у него есть и недостатки:
| Преимущества | Недостатки |
|---|---|
| Полный контроль над UX валидации | Требует написания больше кода |
| Работает во всех современных браузерах | Необходимо реализовывать собственную логику отображения ошибок |
| Позволяет создать единый опыт на разных устройствах | Может конфликтовать с некоторыми фреймворками |
| Поддерживает различные сценарии валидации | При неаккуратной реализации может создать проблемы с доступностью |
Для ещё более тонкого контроля можно дополнительно отслеживать события focusout и blur, чтобы гарантировать, что пользователь всегда сможет покинуть поле, даже если оно содержит ошибку.
Способ 2: Динамическое управление атрибутами валидации в формах
Вместо борьбы с поведением браузера по умолчанию, мы можем временно "обмануть" механизм валидации, динамически управляя атрибутами HTML-элементов. Это менее инвазивный подход, который сохраняет большую часть нативного поведения браузера, но устраняет проблему блокировки фокуса.
Мария Соколова, UX/UI Designer В прошлом году работала над редизайном сервиса онлайн-бронирования для отельного бизнеса. Форма бронирования включала более 15 полей, разделенных на несколько логических блоков. Наше первое решение с агрессивной валидацией полей "на лету" привело к катастрофическим результатам.
Пользователи попросту не могли завершить бронирование, застревая на первых же полях с ошибками. После внедрения техники отложенной валидации с динамическим управлением атрибутами мы увидели рост конверсии на 34%. Ключевым фактором успеха стало то, что пользователи могли заполнить всю форму и увидеть все ошибки сразу, а не решать их последовательно.
Суть этого метода заключается в следующем:
- Временно удаляем атрибуты валидации (required, pattern и т.д.) при фокусировке на поле
- Позволяем пользователю свободно перемещаться между полями
- Возвращаем атрибуты валидации и проверяем данные при отправке формы или в другой подходящий момент
Рассмотрим пример реализации:
document.addEventListener('DOMContentLoaded', () => {
// Сохраняем оригинальные атрибуты валидации всех полей
const formFields = document.querySelectorAll('input, select, textarea');
const validationAttributes = ['required', 'pattern', 'min', 'max', 'minlength', 'maxlength'];
// Хранилище для оригинальных атрибутов
const originalAttributes = new Map();
formFields.forEach(field => {
// Создаем объект для хранения атрибутов конкретного поля
const fieldAttrs = {};
// Сохраняем текущие значения атрибутов
validationAttributes.forEach(attr => {
if (field.hasAttribute(attr)) {
fieldAttrs[attr] = field.getAttribute(attr);
}
});
// Сохраняем в Map, используя поле как ключ
originalAttributes.set(field, fieldAttrs);
// Добавляем обработчики событий
field.addEventListener('focus', removeValidationAttributes);
field.addEventListener('blur', restoreValidationAttributes);
});
// Функция удаления атрибутов валидации при фокусе
function removeValidationAttributes(event) {
const field = event.target;
const attrs = originalAttributes.get(field);
// Удаляем атрибуты валидации
validationAttributes.forEach(attr => {
if (attrs[attr]) {
field.removeAttribute(attr);
}
});
}
// Функция восстановления атрибутов после потери фокуса
function restoreValidationAttributes(event) {
const field = event.target;
const attrs = originalAttributes.get(field);
// Восстанавливаем атрибуты
Object.entries(attrs).forEach(([attr, value]) => {
field.setAttribute(attr, value);
});
// Опционально: проверяем валидность и показываем кастомную ошибку
if (!field.validity.valid) {
field.classList.add('custom-invalid');
// Здесь можно добавить логику отображения ошибок
} else {
field.classList.remove('custom-invalid');
}
}
// Обработчик отправки формы для финальной проверки
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', (event) => {
// Восстанавливаем все атрибуты перед проверкой
formFields.forEach(field => {
const attrs = originalAttributes.get(field);
Object.entries(attrs).forEach(([attr, value]) => {
field.setAttribute(attr, value);
});
});
// Если форма невалидна, предотвращаем отправку
if (!form.checkValidity()) {
event.preventDefault();
// Находим первое невалидное поле для фокуса
const firstInvalidField = form.querySelector(':invalid');
if (firstInvalidField) {
firstInvalidField.focus();
}
}
});
});
});
Этот подход особенно эффективен для сложных многоэтапных форм, где пользователю необходимо заполнить большое количество полей, а валидация должна происходить только на определенных этапах.
Дополнительные возможности этого метода:
- Можно реализовать отложенную валидацию, которая срабатывает только после заполнения всей формы
- Появляется возможность показывать все ошибки валидации сразу, а не по одной
- Пользователь может заполнять поля в удобном для него порядке
- Снижается когнитивная нагрузка, поскольку пользователь не "застревает" на проблемных полях
Способ 3: CSS-решения для корректного отображения фокуса на invalid-элементах
Если предыдущие методы показались слишком сложными или инвазивными, существует третий подход — использование CSS для визуального обозначения ошибок без блокировки фокуса. Это решение особенно удобно, если вы не можете или не хотите существенно вмешиваться в логику работы формы.
Идея заключается в следующем:
- Отключаем стандартные стили валидации браузера
- Создаем собственные стили для различных состояний полей (валидные, невалидные, с фокусом)
- Используем псевдоклассы CSS для визуальной индикации без изменения поведения формы
Вот пример базовой реализации:
/* Отключаем стандартные стили валидации браузера */
input:invalid,
select:invalid,
textarea:invalid {
box-shadow: none !important;
-webkit-box-shadow: none !important;
outline: none !important;
}
/* Стилизуем невалидные поля, когда они теряют фокус */
input:not(:focus):invalid,
select:not(:focus):invalid,
textarea:not(:focus):invalid {
border-color: #ff6b6b;
background-color: rgba(255, 107, 107, 0.05);
}
/* Стилизуем поля с фокусом, даже если они невалидны */
input:focus,
select:focus,
textarea:focus {
border-color: #4dabf7;
box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.25);
outline: none;
}
/* Стилизуем валидные поля */
input:valid:not(:placeholder-shown),
select:valid:not(:focus),
textarea:valid:not(:placeholder-shown) {
border-color: #51cf66;
background-color: rgba(81, 207, 102, 0.05);
}
/* Стилизация сообщений об ошибках */
.form-field {
position: relative;
margin-bottom: 1.5rem;
}
.error-message {
display: none;
position: absolute;
bottom: -1.2rem;
left: 0;
color: #ff6b6b;
font-size: 0.8rem;
}
input:not(:focus):invalid + .error-message,
select:not(:focus):invalid + .error-message,
textarea:not(:focus):invalid + .error-message {
display: block;
}
Для более комплексного решения можно использовать JavaScript вместе с CSS:
// Добавляем атрибут novalidate к форме, чтобы отключить нативную валидацию
document.querySelectorAll('form').forEach(form => {
form.setAttribute('novalidate', '');
form.addEventListener('submit', function(event) {
// Если форма невалидна, предотвращаем отправку
if (!form.checkValidity()) {
event.preventDefault();
// Добавляем класс для визуального отображения всех ошибок
form.classList.add('show-validation-errors');
// Находим все невалидные поля
const invalidFields = form.querySelectorAll(':invalid');
// Отображаем сообщения об ошибках
invalidFields.forEach(field => {
showFieldError(field);
});
// Фокусируемся на первом невалидном поле
invalidFields[0]?.focus();
}
});
// Убираем индикацию ошибки при взаимодействии с полем
form.addEventListener('input', function(event) {
const field = event.target;
if (field.validity.valid) {
hideFieldError(field);
}
});
});
function showFieldError(field) {
// Находим или создаем элемент для сообщения об ошибке
let errorElement = field.nextElementSibling;
if (!errorElement || !errorElement.classList.contains('error-message')) {
errorElement = document.createElement('span');
errorElement.classList.add('error-message');
field.parentNode.insertBefore(errorElement, field.nextSibling);
}
// Устанавливаем текст ошибки в зависимости от типа валидности
if (field.validity.valueMissing) {
errorElement.textContent = 'Заполните это поле';
} else if (field.validity.typeMismatch) {
errorElement.textContent = 'Неверный формат';
} else if (field.validity.patternMismatch) {
errorElement.textContent = field.title || 'Не соответствует шаблону';
} else if (field.validity.tooShort) {
errorElement.textContent = `Минимальная длина: ${field.minLength} символов`;
} else if (field.validity.tooLong) {
errorElement.textContent = `Максимальная длина: ${field.maxLength} символов`;
} else {
errorElement.textContent = 'Некорректное значение';
}
// Добавляем класс ошибки к полю
field.classList.add('has-error');
}
function hideFieldError(field) {
// Удаляем класс ошибки
field.classList.remove('has-error');
// Находим и удаляем сообщение об ошибке
const errorElement = field.nextElementSibling;
if (errorElement && errorElement.classList.contains('error-message')) {
errorElement.textContent = '';
}
}
Это решение особенно элегантно, поскольку не меняет структуру HTML и не требует сложных манипуляций с DOM. Вместо этого оно полагается на CSS для обеспечения визуальной обратной связи.
Ключевые преимущества CSS-подхода:
- Минимальное вмешательство в логику работы формы
- Высокая производительность (стили применяются браузером без дополнительного JavaScript)
- Упрощенное сопровождение и обновление дизайна
- Возможность использования в проектах с ограниченным доступом к JavaScript
- Хорошая совместимость с различными фреймворками
Практические рекомендации по доступности форм при валидации
Независимо от выбранного способа решения проблемы фокусировки на invalid-элементах, следует учитывать ряд практических рекомендаций по доступности. Доступные формы не только улучшают опыт для пользователей с ограниченными возможностями, но и делают интерфейс более удобным для всех. 🔍
Основные принципы доступной валидации форм:
- Четкая обратная связь — пользователь должен понимать, что именно не так с введенными данными
- Множественные индикаторы ошибок — используйте не только цвет, но и иконки, текст, обводку
- Семантическая разметка — связывайте сообщения об ошибках с соответствующими полями с помощью ARIA-атрибутов
- Проактивная помощь — предоставляйте подсказки о требуемом формате до возникновения ошибки
- Возможность исправления — позволяйте пользователю вернуться и исправить ошибки в любом порядке
Ниже приведены конкретные ARIA-атрибуты, которые следует использовать при реализации кастомной валидации:
| ARIA-атрибут | Назначение | Пример использования |
|---|---|---|
| aria-invalid | Указывает, что поле содержит ошибку | <input type="email" aria-invalid="true"> |
| aria-describedby | Связывает поле с описанием ошибки | <input aria-describedby="email-error"> |
| aria-errormessage | Указывает на элемент с сообщением об ошибке | <input aria-errormessage="email-error-text"> |
| aria-live | Объявляет динамический контент скринридерам | <div aria-live="polite" id="validation-summary"> |
Примером комплексного подхода может служить следующий код:
// Функция установки состояния ошибки с учетом доступности
function setFieldErrorState(field, isError, errorMessage) {
// Находим или создаем контейнер для сообщения об ошибке
let errorId = `${field.id}-error`;
let errorElement = document.getElementById(errorId);
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = errorId;
errorElement.className = 'error-message';
errorElement.setAttribute('aria-live', 'polite');
field.parentNode.insertBefore(errorElement, field.nextSibling);
}
if (isError) {
// Устанавливаем состояние ошибки
field.setAttribute('aria-invalid', 'true');
field.setAttribute('aria-describedby', errorId);
errorElement.textContent = errorMessage;
field.classList.add('has-error');
} else {
// Сбрасываем состояние ошибки
field.removeAttribute('aria-invalid');
field.removeAttribute('aria-describedby');
errorElement.textContent = '';
field.classList.remove('has-error');
}
}
// Использование функции для установки ошибки
const emailField = document.getElementById('email');
setFieldErrorState(
emailField,
true,
'Пожалуйста, введите корректный email-адрес в формате example@domain.com'
);
// Использование для сброса ошибки
emailField.addEventListener('input', () => {
if (emailField.validity.valid) {
setFieldErrorState(emailField, false);
}
});
Дополнительные рекомендации по улучшению UX при валидации форм:
- Не показывайте сообщения об ошибках до первого взаимодействия пользователя с полем
- Валидируйте данные не только при отправке формы, но и при потере фокуса (blur), чтобы предоставить мгновенную обратную связь
- Используйте позитивную валидацию — показывайте не только ошибки, но и успешные результаты
- Группируйте связанные поля и ошибки для улучшения когнитивного восприятия
- Предлагайте конкретные решения вместо абстрактных сообщений об ошибках (например, "Введите 6 или более символов" вместо "Неверная длина пароля")
Управление фокусировкой на invalid-элементах — это баланс между стандартами, доступностью и удобством использования. Лучшая стратегия часто включает комбинацию описанных выше методов: CSS для стилизации, JavaScript для контроля поведения и ARIA-атрибуты для доступности. Проводите тестирование своих форм с реальными пользователями, включая тех, кто использует вспомогательные технологии, чтобы убедиться, что ваше решение действительно помогает, а не создает дополнительные барьеры.