Модальное окно временно блокирует взаимодействие пользователя с остальным интерфейсом страницы. Визуально модальное окно располагается поверх основного контента и требует явного действия для закрытия или подтверждения операции. Однако визуального перекрытия недостаточно: необходимо также ограничить навигацию по элементам интерфейса на уровне клавиатурного фокуса.
Без контроля фокуса возникает несколько проблем:
Библиотека Focus-trap решает эту задачу: она «запирает» фокус внутри указанного контейнера и не позволяет покинуть его, пока ловушка фокуса активна.
Focus-trap — небольшая JavaScript-библиотека, предназначенная для ограничения перемещения фокуса внутри конкретного DOM-элемента. Чаще всего используется в:
Основной механизм работы:
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()В базовом сценарии используются первые два метода.
При открытии окна необходимо выполнить две операции:
const openButton = document.getElementById('open-modal');
openButton.addEventListener('click', () => {
modal.hidden = false;
trap.activate();
});
После активации:
Закрытие должно:
const cancelButton = document.getElementById('cancel');
cancelButton.addEventListener('click', () => {
trap.deactivate();
modal.hidden = true;
});
После деактивации библиотека возвращает фокус на элемент, который был активен до открытия модального окна (обычно кнопку открытия).
Это важная особенность доступного интерфейса: пользователь продолжает навигацию с того места, где остановился.
В большинстве интерфейсов модальное окно закрывается клавишей Escape.
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !modal.hidden) {
trap.deactivate();
modal.hidden = true;
}
});
Хотя Focus-trap может автоматически обрабатывать Escape через настройки, базовая реализация часто контролируется вручную.
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 сохраняет элемент, который был активен перед активацией ловушки.
Алгоритм:
document.activeElement;deactivate() фокус возвращается к этому
элементу.Такой механизм особенно важен для пользователей:
Если внутри контейнера нет элементов, способных получать фокус, Focus-trap может использовать сам контейнер.
Для этого контейнеру задаётся tabindex.
<div id="modal" class="modal" hidden tabindex="-1">
Это позволяет программно перемещать фокус на контейнер.
В интерфейсах могут присутствовать вложенные диалоги, например:
Focus-trap поддерживает стек ловушек. Когда новая ловушка активируется, предыдущая автоматически приостанавливается.
Пример последовательности:
Главная страница
↓
Модальное окно
↓
Диалог подтверждения
Фокус перемещается только внутри самого верхнего активного контейнера.
Контроль фокуса — обязательное требование доступных интерфейсов. Помимо 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 возможны ошибки.
Если интерактивные элементы находятся за пределами контейнера, фокус может покидать модальное окно. Контейнер должен полностью охватывать все элементы диалога.
Использование специализированной библиотеки имеет несколько преимуществ:
Надёжность
Реализация корректной циклической навигации вручную требует сложной логики.
Поддержка браузеров
Focus-trap учитывает различия поведения фокуса в разных браузерах.
Минимальный размер
Библиотека остаётся компактной и практически не влияет на размер приложения.
Интеграция с фреймворками
Focus-trap используется в проектах на:
Базовая реализация на чистом JavaScript служит фундаментом для более сложных интерфейсных компонентов.