Валидация форм: решение проблемы блокировки фокуса на ошибках

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

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

    Создавали когда-нибудь форму, где пользователь застревал на одном поле из-за ошибки валидации? 🤦‍♂️ Знакомое чувство досады, когда пользователь жалуется, что не может переключиться между полями, если где-то допустил ошибку? Это не просто раздражающая мелочь, а серьезная проблема доступности, способная уничтожить конверсию. Сегодня разберем, почему браузеры блокируют фокус на невалидных полях и предложу три проверенных способа исправить это поведение — от JavaScript-хаков до элегантных CSS-решений.

Осваиваете тонкости веб-разработки и хотите создавать интерфейсы, которые не просто работают, а действительно помогают пользователям? На курсе Обучение веб-разработке от Skypro вы научитесь не только основам HTML, CSS и JavaScript, но и продвинутым техникам валидации форм с учетом доступности. Наши студенты создают интерфейсы, которые работают для всех — от опытных пользователей до людей с особыми потребностями. Инвестируйте в навыки, которые действительно ценятся на рынке!

Почему браузеры блокируют фокус на недействительных полях формы

Проблема фокусировки на невалидных полях формы — это не баг, а осознанное решение разработчиков браузеров. Когда пользователь пытается переместить фокус с поля, которое не проходит валидацию (имеет атрибут :invalid), браузер "привязывает" фокус к этому полю, требуя исправить ошибку перед продолжением.

Вот что происходит на техническом уровне:

  1. Браузер обнаруживает попытку перемещения фокуса (через Tab или клик) с поля, имеющего состояние :invalid
  2. Срабатывает внутренний механизм удержания фокуса (focus containment)
  3. Браузер предотвращает стандартное поведение события blur и возвращает фокус обратно на невалидное поле
  4. Дополнительно отображается нативное сообщение об ошибке валидации

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

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

Особенно остро проблема проявлялась у пользователей, использующих только клавиатуру. Представьте: человек вводит неправильный email, пытается перейти к следующему полю с помощью Tab, но курсор упрямо возвращается обратно. И так до бесконечности, без явного объяснения причин. Это вызывало фрустрацию и увеличивало отток на этапе регистрации на 23%.

Взглянем на конкретную статистику проблем, связанных с блокировкой фокуса:

Категория пользователей Процент столкнувшихся с проблемой Типичные последствия
Пользователи со скринридерами 87% Полный отказ от заполнения формы
Пользователи с моторными нарушениями 92% Невозможность завершить форму
Обычные пользователи 43% Повышенная фрустрация, отказ от конверсии
Пользователи мобильных устройств 62% Многократные попытки, закрытие страницы

Хотя данное поведение соответствует спецификации HTML5, оно противоречит рекомендациям WCAG (Web Content Accessibility Guidelines), которые подчеркивают важность предоставления пользователям контроля над интерфейсом и возможности исправлять ошибки удобным для них способом.

Пошаговый план для смены профессии

Способ 1: Переопределение поведения событий focus с помощью JavaScript

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

Вот пошаговый алгоритм реализации этого решения:

  1. Отслеживаем событие invalid на элементах формы
  2. Предотвращаем стандартное поведение браузера через preventDefault()
  3. Реализуем собственную логику отображения ошибок
  4. Обеспечиваем возможность перемещения фокуса даже при невалидных данных

Рассмотрим конкретную реализацию:

JS
Скопировать код
// Находим все формы на странице
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%. Ключевым фактором успеха стало то, что пользователи могли заполнить всю форму и увидеть все ошибки сразу, а не решать их последовательно.

Суть этого метода заключается в следующем:

  1. Временно удаляем атрибуты валидации (required, pattern и т.д.) при фокусировке на поле
  2. Позволяем пользователю свободно перемещаться между полями
  3. Возвращаем атрибуты валидации и проверяем данные при отправке формы или в другой подходящий момент

Рассмотрим пример реализации:

JS
Скопировать код
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 для визуального обозначения ошибок без блокировки фокуса. Это решение особенно удобно, если вы не можете или не хотите существенно вмешиваться в логику работы формы.

Идея заключается в следующем:

  1. Отключаем стандартные стили валидации браузера
  2. Создаем собственные стили для различных состояний полей (валидные, невалидные, с фокусом)
  3. Используем псевдоклассы 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:

JS
Скопировать код
// Добавляем атрибут 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-элементах, следует учитывать ряд практических рекомендаций по доступности. Доступные формы не только улучшают опыт для пользователей с ограниченными возможностями, но и делают интерфейс более удобным для всех. 🔍

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

  1. Четкая обратная связь — пользователь должен понимать, что именно не так с введенными данными
  2. Множественные индикаторы ошибок — используйте не только цвет, но и иконки, текст, обводку
  3. Семантическая разметка — связывайте сообщения об ошибках с соответствующими полями с помощью ARIA-атрибутов
  4. Проактивная помощь — предоставляйте подсказки о требуемом формате до возникновения ошибки
  5. Возможность исправления — позволяйте пользователю вернуться и исправить ошибки в любом порядке

Ниже приведены конкретные 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">

Примером комплексного подхода может служить следующий код:

JS
Скопировать код
// Функция установки состояния ошибки с учетом доступности
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-атрибуты для доступности. Проводите тестирование своих форм с реальными пользователями, включая тех, кто использует вспомогательные технологии, чтобы убедиться, что ваше решение действительно помогает, а не создает дополнительные барьеры.

Загрузка...