События фокуса: focus, blur

focus срабатывает когда элемент получает фокус (с клавиатуры или кликом), blur — когда теряет; оба не всплывают, но их всплывающие аналоги focusin/focusout доступны для делегирования.

Зачем нужно

Управление фокусом критично для доступности (a11y) и UX форм. Событие focus используется для подсветки активного поля, blur — для валидации после ввода. Без корректной обработки фокуса приложение неудобно для пользователей клавиатуры и скринридеров.

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

  • Валидация поля после заполнения (по blur)
  • Подсветка активного поля ввода
  • Показ/скрытие подсказок (плейсхолдеры, подписи)
  • Автофокус при открытии модального окна
  • Управление фокусом в SPA при переходах

Базовые события

const input = document.querySelector('#name-input');

input.addEventListener('focus', () => {
  console.log('Поле получило фокус');
  input.parentElement.classList.add('focused');
});

input.addEventListener('blur', () => {
  console.log('Поле потеряло фокус');
  input.parentElement.classList.remove('focused');
  validateField(input); // валидируем после ввода
});

focus vs focusin (всплытие)

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

// focus НЕ всплывает — нельзя делегировать обычным способом
form.addEventListener('focus', () => console.log('focus на form')); // не сработает для input

// focusin всплывает — можно использовать делегирование
form.addEventListener('focusin', (e) => {
  console.log('Фокус получил:', e.target.name);
  e.target.closest('.field')?.classList.add('active');
});

form.addEventListener('focusout', (e) => {
  console.log('Фокус потерял:', e.target.name);
  e.target.closest('.field')?.classList.remove('active');
});

Валидация в реальном времени

function setupFieldValidation(input, validator, errorMessage) {
  const errorEl = document.querySelector(`#${input.id}-error`);

  input.addEventListener('blur', () => {
    const isValid = validator(input.value);
    input.classList.toggle('invalid', !isValid);
    input.classList.toggle('valid', isValid);
    errorEl.textContent = isValid ? '' : errorMessage;
    errorEl.hidden = isValid;
  });

  input.addEventListener('input', () => {
    // При вводе убираем ошибку (покажем снова при blur)
    input.classList.remove('invalid', 'valid');
    errorEl.hidden = true;
  });
}

setupFieldValidation(
  document.querySelector('#email'),
  value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  'Введите корректный email'
);

Управление фокусом программно

// Установить фокус
const firstInput = document.querySelector('input:first-of-type');
firstInput.focus();

// Убрать фокус
input.blur();

// Проверить, есть ли фокус у элемента
if (document.activeElement === input) {
  console.log('Инпут в фокусе');
}

// Показать элемент и поставить фокус (для модального окна)
function openModal(modal) {
  modal.hidden = false;
  const firstFocusable = modal.querySelector('button, input, a, [tabindex]');
  firstFocusable?.focus();
}

Ловушка фокуса (Focus Trap) для модальных окон

function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last  = focusable[focusable.length - 1];

  modal.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey) {
      // Shift+Tab: если первый элемент — переходим к последнему
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      // Tab: если последний элемент — переходим к первому
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  });
}

tabindex

// tabindex="0" — элемент участвует в Tab-навигации
// tabindex="-1" — фокус только программный (focus), не через Tab
// tabindex="1" и выше — явный порядок (не рекомендуется)

const div = document.querySelector('.card');
div.setAttribute('tabindex', '0'); // div теперь фокусируемый
div.focus();

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

1. Делегирование focus через addEventListener на родителе

// focus не всплывает — это не сработает
container.addEventListener('focus', handler); // не поймает фокус потомков

// Правильно: focusin
container.addEventListener('focusin', handler);

// Или: capture phase
container.addEventListener('focus', handler, true); // третий аргумент = capture

2. focus до добавления в DOM

const input = document.createElement('input');
input.focus(); // ничего не произойдёт — элемент не в DOM

document.body.appendChild(input);
input.focus(); // теперь работает

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

Ресурсы