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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

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

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

Этапы разработки

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

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1368
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1255
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    963
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1199
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    942
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    956

Реализация 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.

Доступность сайтов: WCAG, скринриндеры, клавиатурная навигация

На сайте крупного банка кнопка «Подать заявку» в разметке была <div class="btn" onclick="...">. Скринридер NVDA её не анонсировал, Tab пропускал, Enter не срабатывал. Для тысяч незрячих пользователей этот банк просто не существовал как онлайн-сервис. Мы видим такие проблемы каждый день в десятках проектов — и разработка доступных сайтов по стандарту WCAG 2.2 AA становится единственным способом избежать дискриминации и юридических рисков. Штрафы за недоступность для юрлиц достигают 300 000 ₽, а судебные иски — миллионы.

В этой карточке — как мы делаем веб-доступность a11y работающей, на реальных кейсах, с конкретным стеком и цифрами. Без общих фраз.

Почему семантическая разметка — основа веб-доступности a11y?

Большинство проблем доступности решается правильным HTML, а не дополнительными ARIA-атрибутами. <button> вместо <div onclick>, <nav> вместо <div class="navigation">, <h1><h6> в правильной иерархии, <label for="field-id"> вместо <div class="label">. Это базовый уровень, но на практике каждая вторая форма в российских интернет-магазинах не имеет корректных <label>.

ARIA нужна там, где нативный HTML не справляется: кастомные компоненты — выпадающие меню, тултипы, модальные окна, табы, accordion. И вот тут начинается сложность.

Типичная ошибка в кастомных дропдаунах: скринридер не знает, что это combobox, не объявляет количество опций, не говорит какая выбрана, фокус не переходит в список при открытии. Правильная реализация:

  • role="combobox" на инпуте
  • aria-expanded="true/false" при открытии/закрытии
  • aria-controls="listbox-id" указывает на список
  • aria-activedescendant — ID текущего выбранного элемента
  • role="option" и aria-selected на каждом варианте

Это не теория, это то, что тестируется скринридером. NVDA + Chrome или VoiceOver + Safari — обязательная часть QA.

Пример реализации кастомного комбобокса с ARIA
<div role="combobox" aria-expanded="false" aria-controls="listbox-1" aria-activedescendant="" tabindex="0">
  <label for="input-1">Выберите город</label>
  <input id="input-1" type="text" role="combobox" aria-autocomplete="list" />
  <ul id="listbox-1" role="listbox" aria-label="Города">
    <li role="option" aria-selected="false" id="opt-1">Москва</li>
    <li role="option" aria-selected="false" id="opt-2">Санкт-Петербург</li>
  </ul>
</div>

Стоимость исправления одного нарушения уровня A — от 5 000 до 15 000 ₽ в зависимости от сложности. Внедрение a11y с этапа проектирования сокращает бюджет на рефакторинг в 2–3 раза по сравнению с доработкой готового сайта.

Как правильно построить клавиатурную навигацию?

Tab-порядок должен совпадать с визуальным порядком элементов. Если в HTML кнопка «Отмена» стоит перед «Подтвердить», но CSS их меняет местами — пользователь клавиатуры в замешательстве.

Focus trap в модальных окнах. Когда модалка открывается, Tab должен циклиться только внутри неё. При закрытии — возврат фокуса на элемент, который открыл модалку. Без этого пользователь после закрытия оказывается в начале страницы.

tabindex="-1" — элемент не попадает в Tab-последовательность, но может получить фокус программно. Используется для элементов, которые получают фокус через JavaScript (заголовки секций после навигации по якорям).

tabindex="1" и выше — почти всегда ошибка. Явный порядок ломает естественный и создаёт непредсказуемое поведение. Управляйте порядком через DOM, не через tabindex.

Skip links — ссылка «Перейти к содержимому», скрытая визуально, видимая при Tab. Позволяет пользователям скринридеров пропустить повторяющуюся навигацию.

Цвет и контраст: требования и частые нарушения

WCAG 2.2 AA требует контраст 4.5:1 для обычного текста, 3:1 для крупного (18px+ или 14px+ bold). AAA требует 7:1 и 4.5:1.

Самые частые нарушения: серый placeholder в инпутах (#999 на белом = 2.9:1), светло-серый secondary текст, белый текст на пастельном фоне.

Цвет не должен быть единственным индикатором: «поля обязательные выделены красным» без звёздочки — нарушение для людей с цветовой слепотой.

Инструменты проверки: axe DevTools, WAVE, Accessibility Inspector в Chrome DevTools. axe-core интегрируется в Playwright-тесты: автоматическая проверка на 80+ правил при каждом деплое. Ручное тестирование находит примерно на 60% больше ошибок, чем автоматическое.

Медиаконтент и динамика: что важно?

Изображения без alt — частый базовый провал. alt должен быть смысловым: не alt="image_123.jpg", а описание содержимого, релевантное контексту. Декоративные изображения — alt="" (пустой, не отсутствующий атрибут).

Видео должно иметь субтитры. YouTube автосубтитры — не стандарт, они ошибаются. WebVTT-файлы с корректными субтитрами для всего образовательного и маркетингового видеоконтента.

Анимации — проблема для пользователей с вестибулярными расстройствами. @media (prefers-reduced-motion: reduce) — медиа-запрос, отключающий или замедляющий анимации для пользователей с такой настройкой в ОС.

Что изменилось в WCAG 2.2?

Версия 2.2 вступила в силу с новыми критериями:

Критерий Уровень Суть
2.5.7 Dragging Movements AA Все drag-операции должны иметь клавиатурную альтернативу
2.5.8 Target Size AA Минимальный размер интерактивного элемента 24×24 px
3.2.6 Consistent Help A Расположение контакта/чата должно быть одинаковым на всех страницах
3.3.7 Redundant Entry A Не заставлять вводить одну информацию дважды в одной сессии

Эти критерии повышают порог входа, но мы уже включаем их в стандартный чек-лист.

Уровень Минимальный контраст текста Контраст крупного текста
AA 4.5:1 3:1
AAA 7:1 4.5:1

Аудит и устранение нарушений

Автоматические инструменты находят около 30–40% нарушений. Остальное — только ручное тестирование. Минимальный сценарий: пройти весь критический user flow (регистрация, покупка, форма) только клавиатурой и со скринридером.

Процесс работы

  1. Автоматический аудит — axe-core, Lighthouse, WAVE — выдача 80+ правил.
  2. Ручное тестирование — NVDA, VoiceOver, клавиатура — 2–3 дня на типовой сайт.
  3. Приоритизация нарушений — P1 (блокирует использование), P2 (создаёт сложности), P3 (улучшения).
  4. Исправление — итерациями, встраиваем проверки в CI через Playwright + axe.
  5. Повторный аудит — закрытие всех P1/P2 перед релизом.
  6. Документация и передача — отчёт с результатами, рекомендации по поддержке, обучение команды.

Результаты и объём работ

  • Полный отчёт по аудиту с приоритизацией нарушений (PDF/HTML)
  • Исправленный код: семантическая разметка, ARIA, клавиатурная навигация
  • Интеграция axe-core в CI/CD для регрессионного контроля
  • Обучение разработчиков заказчика по работе с a11y (2-часовая сессия)
  • Доступ к репозиторию с примерами корректных компонентов
  • Гарантия соответствия WCAG 2.2 AA на момент сдачи

Сроки

Этап Длительность
Аудит сайта (до 50 страниц) 3–7 дней
Устранение нарушений A/AA на существующем проекте 3–8 недель
Разработка нового проекта с соблюдением WCAG 2.2 AA от 6 недель

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

Опыт и гарантии

Мы занимаемся веб-доступностью a11y более 8 лет. Реализовали более 50 проектов для банков, ритейла и госсектора. Сертифицированные специалисты (IAAP CPACC, WAS). Гарантируем прохождение аудита третьей стороной или дорабатываем бесплатно.

Стандарт WCAG 2.2 — официальная рекомендация W3C, определяющая требования к доступности веб-контента.

Wikipedia: Web Content Accessibility Guidelines
Wikipedia: ARIA

Уровни веб-доступности a11y по стандарту WCAG 2.2: A, AA, AAA — уровни веб-доступности a11y по версии 2.2.

Закажите аудит сейчас — получите чек-лист и предварительную оценку бесплатно. Пишите в Telegram или на почту — ответим в течение часа.