Базовая реализация модального окна

Модальное окно временно блокирует взаимодействие пользователя с остальным интерфейсом страницы. Визуально модальное окно располагается поверх основного контента и требует явного действия для закрытия или подтверждения операции. Однако визуального перекрытия недостаточно: необходимо также ограничить навигацию по элементам интерфейса на уровне клавиатурного фокуса.

Без контроля фокуса возникает несколько проблем:

  • клавиша Tab продолжает перемещать фокус по элементам страницы за пределами модального окна;
  • пользователи клавиатурной навигации теряют контекст текущего действия;
  • нарушаются требования доступности (accessibility);
  • скринридеры могут переходить к элементам, которые должны быть временно недоступны.

Библиотека Focus-trap решает эту задачу: она «запирает» фокус внутри указанного контейнера и не позволяет покинуть его, пока ловушка фокуса активна.


Общая идея библиотеки Focus-trap

Focus-trap — небольшая JavaScript-библиотека, предназначенная для ограничения перемещения фокуса внутри конкретного DOM-элемента. Чаще всего используется в:

  • модальных окнах;
  • диалогах подтверждения;
  • боковых панелях;
  • выпадающих меню;
  • интерфейсах мастеров (wizard).

Основной механизм работы:

  1. создаётся ловушка фокуса внутри контейнера;
  2. библиотека определяет все фокусируемые элементы;
  3. при нажатии Tab или Shift + Tab навигация циклически перемещается внутри контейнера;
  4. при закрытии интерфейса ловушка деактивируется и фокус возвращается к исходному элементу.

Установка библиотеки

Focus-trap распространяется через npm.

npm install focus-trap

Подключение в модуле:

import { createFocusTrap } from 'focus-trap';

Для проектов без сборщика возможно использование через CDN, однако в современных приложениях чаще применяется модульная сборка.


Структура модального окна

Базовая реализация начинается с корректной HTML-структуры. Модальное окно должно содержать контейнер, внутри которого располагаются интерактивные элементы.

<button id="open-modal">Открыть окно</button>

<div id="modal" class="modal" hidden>
  <div class="modal-content">
    <h2>Подтверждение действия</h2>

    <p>Удалить выбранный файл?</p>

    <button id="confirm">Удалить</button>
    <button id="cancel">Отмена</button>
  </div>
</div>

Ключевые элементы:

  • кнопка открытия окна;
  • контейнер модального окна;
  • интерактивные элементы внутри него (кнопки, поля ввода и т.д.).

Атрибут hidden используется для скрытия модального окна до момента активации.


Базовые стили

Минимальные стили обеспечивают визуальное перекрытие основного интерфейса.

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  
  display: flex;
  align-items: center;
  justify-content: center;

  background: rgba(0,0,0,0.5);
}

.modal-content {
  background: white;
  padding: 24px;
  border-radius: 6px;
  min-width: 300px;
}

Основные задачи стилей:

  • затемнение фона;
  • центрирование модального окна;
  • визуальное отделение содержимого.

Создание ловушки фокуса

После подготовки HTML и CSS создаётся экземпляр focus trap.

const modal = document.getElementById('modal');

const trap = createFocusTrap(modal);

Функция createFocusTrap принимает DOM-элемент, внутри которого должен быть ограничен фокус.

Созданный объект содержит несколько методов управления:

  • activate()
  • deactivate()
  • pause()
  • unpause()

В базовом сценарии используются первые два метода.


Открытие модального окна

При открытии окна необходимо выполнить две операции:

  1. показать контейнер;
  2. активировать ловушку фокуса.
const openButton = document.getElementById('open-modal');

openButton.addEventListener('click', () => {
  modal.hidden = false;
  trap.activate();
});

После активации:

  • фокус автоматически перемещается на первый доступный элемент внутри контейнера;
  • клавиша Tab перемещает фокус только внутри модального окна;
  • элементы основной страницы становятся недоступны для клавиатурной навигации.

Закрытие модального окна

Закрытие должно:

  1. деактивировать ловушку;
  2. скрыть модальное окно.
const cancelButton = document.getElementById('cancel');

cancelButton.addEventListener('click', () => {
  trap.deactivate();
  modal.hidden = true;
});

После деактивации библиотека возвращает фокус на элемент, который был активен до открытия модального окна (обычно кнопку открытия).

Это важная особенность доступного интерфейса: пользователь продолжает навигацию с того места, где остановился.


Управление клавишей Escape

В большинстве интерфейсов модальное окно закрывается клавишей Escape.

document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !modal.hidden) {
    trap.deactivate();
    modal.hidden = true;
  }
});

Хотя Focus-trap может автоматически обрабатывать Escape через настройки, базовая реализация часто контролируется вручную.


Поведение клавиши Tab

Focus-trap анализирует DOM-дерево контейнера и определяет все tabbable элементы, то есть элементы, доступные для фокуса:

  • <button>
  • <a>
  • <input>
  • <select>
  • <textarea>
  • элементы с tabindex

Если пользователь нажимает Tab на последнем элементе:

Последний элемент → Tab → первый элемент

Если используется Shift + Tab на первом элементе:

Первый элемент → Shift+Tab → последний элемент

Таким образом создаётся циклическая навигация.


Полный пример реализации

import { createFocusTrap } from 'focus-trap';

const modal = document.getElementById('modal');
const openButton = document.getElementById('open-modal');
const cancelButton = document.getElementById('cancel');

const trap = createFocusTrap(modal);

openButton.addEventListener('click', () => {
  modal.hidden = false;
  trap.activate();
});

cancelButton.addEventListener('click', closeModal);

document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !modal.hidden) {
    closeModal();
  }
});

function closeModal() {
  trap.deactivate();
  modal.hidden = true;
}

Этот пример демонстрирует базовый сценарий использования:

  • открытие диалога;
  • ограничение фокуса;
  • закрытие интерфейса.

Автоматическая установка фокуса

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

Иногда требуется явно указать начальный элемент. Для этого используется параметр initialFocus.

const trap = createFocusTrap(modal, {
  initialFocus: '#confirm'
});

В этом случае при открытии модального окна фокус сразу перемещается на кнопку подтверждения.


Возврат фокуса после закрытия

Focus-trap сохраняет элемент, который был активен перед активацией ловушки.

Алгоритм:

  1. пользователь нажимает кнопку открытия;
  2. библиотека запоминает текущий document.activeElement;
  3. после deactivate() фокус возвращается к этому элементу.

Такой механизм особенно важен для пользователей:

  • клавиатурной навигации;
  • вспомогательных технологий;
  • скринридеров.

Обработка отсутствия фокусируемых элементов

Если внутри контейнера нет элементов, способных получать фокус, Focus-trap может использовать сам контейнер.

Для этого контейнеру задаётся tabindex.

<div id="modal" class="modal" hidden tabindex="-1">

Это позволяет программно перемещать фокус на контейнер.


Управление несколькими модальными окнами

В интерфейсах могут присутствовать вложенные диалоги, например:

  • модальное окно подтверждения внутри другого окна;
  • настройки внутри панели.

Focus-trap поддерживает стек ловушек. Когда новая ловушка активируется, предыдущая автоматически приостанавливается.

Пример последовательности:

Главная страница
   ↓
Модальное окно
   ↓
Диалог подтверждения

Фокус перемещается только внутри самого верхнего активного контейнера.


Важность доступности (Accessibility)

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

Пример:

<div
  id="modal"
  class="modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
>

Основные атрибуты:

  • role="dialog" — обозначает диалоговое окно;
  • aria-modal="true" — сообщает скринридерам о модальном режиме;
  • aria-labelledby — связывает заголовок с диалогом.

Эти атрибуты улучшают восприятие интерфейса вспомогательными технологиями.


Типичные ошибки при реализации

Отсутствие возврата фокуса

Если ловушка фокуса отключается без возврата к предыдущему элементу, пользователь может потерять контекст навигации.


Фокус на скрытых элементах

Если внутри контейнера присутствуют элементы со стилем display: none, они не должны участвовать в навигации. Focus-trap учитывает это автоматически, но при динамическом изменении DOM возможны ошибки.


Неправильная структура DOM

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


Преимущества использования Focus-trap

Использование специализированной библиотеки имеет несколько преимуществ:

Надёжность

Реализация корректной циклической навигации вручную требует сложной логики.

Поддержка браузеров

Focus-trap учитывает различия поведения фокуса в разных браузерах.

Минимальный размер

Библиотека остаётся компактной и практически не влияет на размер приложения.

Интеграция с фреймворками

Focus-trap используется в проектах на:

  • React
  • Vue
  • Angular
  • Svelte

Базовая реализация на чистом JavaScript служит фундаментом для более сложных интерфейсных компонентов.