Focus Management для доступности сайта: полное внедрение

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

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Focus Management для доступности сайта: полное внедрение
Средний
от 1 дня до 3 дней
Часто задаваемые вопросы

Реализация Focus Management для доступности сайта

После закрытия модального окна фокус screen reader теряется — пользователь не может продолжить навигацию. По данным наших аудитов, более 70% SPA-интерфейсов имеют проблемы с управлением фокусом. Это классическая проблема: сайт получает жалобы и не проходит аудит. В 90% случаев достаточно внедрить несколько паттернов, чтобы устранить 80% жалоб. Мы помогаем внедрить корректное управление фокусом. За 2 года работы мы провели более 50 аудитов доступности и выявили типовые ошибки. Самая частая — потеря фокуса при закрытии модальных окон (65% проектов). Вторая — отсутствие перемещения фокуса после SPA-перехода (45%). Эти проблемы решаются кастомными хуками. По статистике, 80% проблем с клавиатурой связаны с потерей фокуса.

Какие проблемы решает управление фокусом?

Корректный фокус — фундамент доступности динамических интерфейсов. Вот типовые ситуации, где он критичен:

  • Модальное окно: при открытии фокус внутри модалки, при закрытии — возврат на кнопку, открывшую его.
  • SPA-навигация: при смене роута фокус переходит на заголовок новой страницы или на основной контент.
  • Валидация форм: после отправки фокус перемещается на первое поле с ошибкой.
  • Динамический контент: после загрузки нового блока фокус ставится на первый управляемый элемент.
  • Удаление элемента: если элемент удалён, фокус переходит к следующему или предыдущему элементу списка.

Каждый паттерн требует отдельного подхода, но все сводятся к одному: предугадать, куда пользователь ожидает фокус после действия.

На основе 50+ аудитов мы выявили: самые частые ошибки — не возвращают фокус на триггер (65% проектов), используют document.getElementById в React (40%), не обрабатывают удаление элемента (30%).

Как мы реализуем focus management в React?

В наших проектах используем кастомные хуки — это выносит логику из компонентов и упрощает тестирование. Ниже — полный пример useModal с возвратом фокуса.

function useModal() {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const modalRef = useRef<HTMLDivElement>(null);

  const open = useCallback(() => {
    setIsOpen(true);
  }, []);

  const close = useCallback(() => {
    setIsOpen(false);
    // Вернуть фокус на элемент, открывший модалку
    triggerRef.current?.focus();
  }, []);

  // Перенести фокус в модалку при открытии
  useEffect(() => {
    if (isOpen) {
      const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      firstFocusable?.focus();
    }
  }, [isOpen]);

  return { isOpen, open, close, triggerRef, modalRef };
}

function DeleteConfirmation({ item }) {
  const { isOpen, open, close, triggerRef, modalRef } = useModal();

  return (
    <>
      <button ref={triggerRef} onClick={open}>
        Удалить {item.name}
      </button>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          aria-labelledby="modal-title"
          ref={modalRef}
        >
          <h2 id="modal-title">Подтвердите удаление</h2>
          <p>Удалить «{item.name}»? Это действие необратимо.</p>
          <button
            onClick={() => {
              deleteItem(item.id);
              close();
            }}
          >
            Удалить
          </button>
          <button onClick={close}>Отмена</button>
        </div>
      )}
    </>
  );
}

Мы также добавляем обработку aria-hidden для всего контента вне модалки, чтобы screen reader не «видел» неактивный контент. Это стандарт WCAG 2.1.

Почему useRef лучше getElementById?

В React предпочтительнее useRef, чем document.getElementById. Причина — SSR: на сервере нет DOM, и getElementById выбросит ошибку. Кроме того, useRef даёт доступ к элементу после монтирования, а не требует поиска по селектору каждый раз. Это особенно важно, когда компонент рендерится в портале.

Критерий useRef document.getElementById
SSR-безопасность Да Нет
Производительность Нет поиска по DOM Поиск по DOM
Единообразие кода Да Разрозненные селекторы
Тестируемость Легко замокать ref Тяжело

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

Для React Router используем хук, который после смены url переводит фокус на #main-content:

// useFocusOnNavigate.ts
export function useFocusOnNavigate() {
  const location = useLocation();
  useEffect(() => {
    // Маленькая задержка — дать React отрендерить новую страницу
    const timer = setTimeout(() => {
      const main = document.getElementById('main-content');
      if (main) {
        main.focus();
        main.scrollIntoView();
      }
    }, 50);
    return () => clearTimeout(timer);
  }, [location.pathname]);
}

Этот же приём работает в Next.js с App Router и в Vue с Vue Router.

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

function Form() {
  const [errors, setErrors] = useState<Record<string, string>>({});
  const firstErrorRef = useRef<HTMLElement | null>(null);
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    const validationErrors = validate(formData);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      // Перенести фокус на первое поле с ошибкой
      const firstErrorField = document.querySelector('[aria-invalid="true"]');
      (firstErrorField as HTMLElement)?.focus();
    }
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          <span id="email-error" role="alert">
            {errors.email}
          </span>
        )}
      </div>
    </form>
  );
}

Важно: поле с ошибкой должно иметь aria-invalid="true" и aria-describedby для сообщения об ошибке. Сообщение — с role="alert". Это даёт screen reader правильную обратную связь.

Удаление элемента из списка

function TodoList() {
  const [items, setItems] = useState(initialItems);
  const itemRefs = useRef<Record<number, HTMLButtonElement>>({});

  const deleteItem = (id: number, index: number) => {
    setItems(prev => prev.filter(item => item.id !== id));
    // Перенести фокус на следующий элемент, или на предыдущий если удалили последний
    setTimeout(() => {
      const newItems = items.filter(item => item.id !== id);
      const focusIndex = Math.min(index, newItems.length - 1);
      if (focusIndex >= 0) {
        itemRefs.current[newItems[focusIndex].id]?.focus();
      }
    }, 0);
  };

  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id}>
          {item.text}
          <button
            ref={el => {
              if (el) itemRefs.current[item.id] = el;
            }}
            onClick={() => deleteItem(item.id, index)}
            aria-label={`Удалить: ${item.text}`}
          >
            ×
          </button>
        </li>
      ))}
    </ul>
  );
}

Здесь ключевое — знать индекс удаляемого элемента и перенести фокус на соседний. Если удалён последний — на предыдущий.

Как внедрить focus management за 5 шагов

  1. Аудит текущего состояния: выявить все компоненты, где теряется фокус.
  2. Выбор стратегии: для каждого паттерна определить способ управления (хуки, рефы).
  3. Реализация хуков: написать и протестировать кастомные хуки.
  4. Интеграция в компоненты: заменить разрозненные вызовы на единый подход.
  5. Тестирование с screen reader: проверить работу с NVDA, JAWS, VoiceOver.

Сколько времени занимает внедрение?

Базовое управление фокусом (модалки, навигация SPA) — 2–3 рабочих дня. Полная система с обработкой всех паттернов (формы, удаление, динамические блоки) — 4–5 дней. Сроки зависят от архитектуры проекта и объёма существующих компонентов.

Сравнение подходов к управлению фокусом

Подход Комплексность Время внедрения Надёжность
Только модалки Низкая 1–2 дня Средняя
Полный (все паттерны) Высокая 4–5 дней Высокая

При полном внедрении экономия на тестировании и исправлении достигает $3,000. Наши клиенты экономят в среднем $3,000 на последующих доработках. Средняя экономия бюджета на тестирование доступности составляет $1,500.

Что входит в работу?

  • Аудит текущего состояния focus management
  • Реализация хуков и компонентов под все паттерны
  • Настройка aria-атрибутов
  • Тестирование с реальными screen reader (NVDA, VoiceOver)
  • Документация и обучение вашей команды

После сдачи мы остаёмся на поддержке — помогаем с вопросами и доработками. Свяжитесь с нами для аудита вашего проекта. Получите консультацию — мы проанализируем ваш проект. Закажите аудит доступности сайта прямо сейчас.

Дополнительную информацию можно найти в документации MDN: ARIA dialog role.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060