Посетитель открывает окно с требованиями к проекту. Визуально оно поверх страницы, но после Tab фокус уходит к ссылке в футере, скринридер не сообщает название окна, а закрыть его на телефоне можно только случайным нажатием на фон. Это проблемы пользовательского пути: красивое затемнение ещё не создаёт модальный диалог.
Сначала решите, нужно ли модальное окно
Если человек читает длинную программу, сравнивает цены или ищет материал, обычная страница либо встроенный раздел часто удобнее. У них есть адрес, история браузера и место для большого текста. Диалог уместен для короткой задачи, временно требующей внимания. Не прячьте важные условия услуги только в окне, которое нужно сначала обнаружить и открыть.
Перед реализацией запишите три действия: откуда окно открывается, что человек должен сделать внутри и куда возвращается после закрытия. Для подтверждения удаления начальный фокус на безопасном действии может быть полезнее, чем на разрушительной кнопке. Для большого информационного сообщения сначала важен контекст. Решение зависит от содержания, поэтому правило «всегда первый input» слишком грубое.
Минимальный пример с нативным dialog
Этот пример можно сохранить в отдельный HTML-файл. Кнопка открывает окно через showModal. Название связано с заголовком через aria-labelledby. Форма method="dialog" закрывает окно без сетевого запроса. В небольшом информационном окне начальный фокус направлен на видимую кнопку закрытия: посетитель получает понятный выход и может прочитать короткий текст перед ней.
TSX<button id="open-review" type="button">Требования к ревью</button>
<dialog id="review-dialog" aria-labelledby="review-title">
<h2 id="review-title">Что подготовить к ревью</h2>
<p>Ссылка на проект, команда запуска и один конкретный вопрос.</p>
<form method="dialog">
<button id="close-review" autofocus>Вернуться к странице</button>
</form>
</dialog>
<script>
const opener = document.querySelector('#open-review');
const dialog = document.querySelector('#review-dialog');
opener.addEventListener('click', () => {
if (!dialog.open) dialog.showModal();
});
</script>
Вызов showModal даёт браузеру информацию о модальности. Фон становится inert, а окно появляется в верхнем слое. Если просто изменить CSS на display: block или добавить open, такой же модальности не получится. Не добавляйте tabindex к самому dialog. Нативный элемент уже предоставляет нужную семантику; дополнительные атрибуты должны объяснять контент, а не дублировать её.
Escape, cancel и возврат к инициатору
При обычном модальном диалоге Escape инициирует событие cancel и закрытие. Если отменить cancel через preventDefault, стандартное закрытие перестанет работать. Делайте это только при понятной необходимости, например когда нужно отдельно подтвердить отказ от несохранённого ввода. Даже тогда человек должен видеть способ выйти. Блокировка Escape ради анимации легко создаёт ловушку.
Нативный диалог обычно возвращает фокус к ранее активному элементу. Проверьте это в своём сценарии. Если инициатор исчез после операции, например карточка была удалена, заранее выберите логичное следующее место: соседний элемент или заголовок списка. Не вызывайте focus на отключённой кнопке и не отправляйте человека в начало всей страницы без причины.
Интеграция с React
В React храните ссылку на реальный HTMLDialogElement и открывайте его в обработчике действия либо синхронизируйте с состоянием через эффект. Если состояние приложения считает окно открытым, а Escape уже закрыл DOM-элемент, эти два источника расходятся. Обработайте close и приведите состояние в соответствие. Не записывайте open в JSX одновременно с независимыми вызовами showModal: сначала выберите, какая часть кода управляет жизненным циклом.
Длинный текст, прокрутка и маленький экран
Окно должно помещаться в viewport и сохранять доступ к закрытию при увеличении текста. Задайте разумную максимальную ширину, внутренние поля и возможность прокрутить содержимое. Проверьте телефон в горизонтальной ориентации и экранную клавиатуру. Фиксированная высота из макета может превратить нижнюю кнопку в недоступную область, особенно если у окна несколько полей.
Затемнение ::backdrop помогает обозначить контекст, но должно оставлять текст самого окна читаемым. Анимацию можно добавить после проверки основного сценария. Учитывайте prefers-reduced-motion и не откладывайте установку фокуса до окончания долгого перехода: клавиатурное действие должно иметь понятный результат сразу.
Проверка, которая оценивает весь путь
- Сфокусируйте кнопку открытия клавиатурой и нажмите Enter. Проверьте видимое окно, доступное имя и начальный фокус внутри.
- Нажимайте Tab и Shift+Tab. Ссылки на фоне не должны становиться активным местом взаимодействия.
- Закройте окно Escape. Фокус должен вернуться к инициатору, если он ещё доступен.
- Откройте снова и закройте видимой кнопкой. Проверьте этот путь на сенсорном устройстве.
- Повторите несколько открытий и закрытий. Убедитесь, что нет лишних обработчиков и исключений InvalidStateError.
- Проверьте масштаб, длинный текст и чтение скринридером. Автоматический аудит не показывает весь опыт взаимодействия.
Для теста возврата фокуса сначала явно сфокусируйте инициатор. Это устраняет зависимость от различий браузеров при клике мышью. Проверяйте activeElement и доступное имя, а не только наличие атрибута open. Для собственного div-диалога обязанностей больше: семантика, inert-фон, управление фокусом и клавиатурой требуют отдельной реализации и проверки.
Документация и следующая задача
Нативные методы и ограничения: MDN - dialog. Рекомендации для начального фокуса и возврата: WAI-ARIA - Dialog Modal Pattern. Проверьте пример в поддерживаемых браузерах своего продукта: эта статья не обещает одинакового поведения всех ассистивных технологий.
Следующая задача - доступная форма внутри окна. Подумайте, где показывается ошибка, как сохраняется ввод и как вернуться к её полю. Для обсуждения решения используйте критерии frontend-проекта и программу менторства по React.