Журнал • знания • практикаПоиск по сайту

Сайты, дизайн и самостоятельная работа

Найти материал →

Обработка клика вне модального окна и закрытие по escape в веб-разработке

6 минут чтения

Обработка клика вне модального окна и закрытие по escape в веб-разработке

Чтобы закрывать модальное окно по клику вне него, обрабатывайте событие на внешнем слое и закрывайте диалог только тогда, когда целью клика был именно этот слой. Для клавиши Escape проверяйте, что окно открыто. Такой подход для модального окна на JavaScript снижает риск случайного закрытия при клике по содержимому и конфликтов с другими обработчиками.

Логика закрытия модального окна: что важно учесть

  • Разделяйте внешний фон, содержимое диалога и обработку клавиатуры.
  • Сравнивайте event.target с элементом фона: клик по дочернему элементу не должен закрывать окно.
  • Перед закрытием проверяйте текущее состояние, чтобы повторные события не запускали обработчик несколько раз.
  • При открытии перемещайте фокус в диалог, а при закрытии возвращайте его на кнопку открытия.
  • Проверьте поведение мышью, клавиатурой и на сенсорном устройстве.

Как отличить клик по фону от клика внутри модального окна

Закрытие по клику на фон подходит для небольших диалогов, например с кратким сообщением или настройкой. Оно может быть нежелательным для форм с несохранёнными данными, многошаговых сценариев и окон, где случайное закрытие прерывает важное действие. В таких случаях используйте явную кнопку закрытия или запрос подтверждения.

Для различения кликов удобно поместить диалог внутрь отдельного слоя: обработчик слоя сравнивает цель события с самим слоем. Так можно реализовать, как закрыть модальное окно по клику вне его, не закрывая окно при нажатии на текст, ссылку или поле формы.

Обработчик клика вне диалога без случайного закрытия

Для примера ниже нужны доступ к HTML-разметке диалога, кнопке открытия и JavaScript-файлу, который подключён после разметки либо запускается после загрузки DOM. Это базовый вариант создания модального окна HTML CSS JavaScript без сторонних библиотек.



<div id="modal" class="modal" aria-hidden="true">
  <section
    class="modal__dialog"
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    tabindex="-1"
  >
    <h2 id="modal-title">Настройки</h2>
    <p>Содержимое модального окна.</p>
    
  </section>
</div>

Внешний слой должен занимать область окна браузера. Например, задайте ему фиксированное позиционирование, растяните по краям и оформите фон через CSS. Содержимое диалога расположите внутри слоя, а не рядом с ним.

.modal {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgb(0 0 0 / 50%);
  z-index: 1000;
}

.modal[aria-hidden="true"] {
  display: none;
}

.modal__dialog {
  max-width: 36rem;
  width: min(90vw, 36rem);
  padding: 1.5rem;
  background: white;
}

Закрытие по Escape с учётом открытого состояния

Перед добавлением обработчиков учитывайте ограничения: не закрывайте диалог во время обязательного подтверждения; не перехватывайте Escape у другого активного компонента; избегайте параллельного управления одним окном несколькими скриптами; при вложенных диалогах закрывайте только верхний.

  1. Получите ссылки на элементы. Найдите слой, внутренний диалог и кнопки. Проверьте в консоли, что селекторы соответствуют разметке.
  2. Задайте единое состояние. Используйте функцию закрытия, которая ничего не делает, если окно уже закрыто. Перед скрытием запомните кнопку, на которую нужно вернуть фокус.
  3. Откройте диалог и перенесите фокус. Покажите слой, обновите атрибут доступности и установите фокус на диалог или первый интерактивный элемент.
  4. Закройте по клику на фон. Слушайте клик на внешнем слое и вызывайте закрытие только при условии event.target === modal.
  5. Обработайте Escape. На событии keydown проверьте код клавиши и открытое состояние. Удаляйте клавиатурный обработчик при закрытии, чтобы не накапливать слушатели.
  6. Проверьте возврат фокуса. После закрытия снова сфокусируйте кнопку открытия, если она доступна.
const modal = document.querySelector("#modal");
const dialog = modal.querySelector(".modal__dialog");
const openButton = document.querySelector("#open-dialog");
const closeButton = document.querySelector("#close-dialog");

let isOpen = false;
let previousFocus = null;

function closeModal() {
  if (!isOpen) return;

  isOpen = false;
  modal.setAttribute("aria-hidden", "true");
  document.removeEventListener("keydown", onKeydown);

  if (previousFocus?.isConnected) {
    previousFocus.focus();
  }
}

function onKeydown(event) {
  if (event.key === "Escape" && isOpen) {
    closeModal();
  }
}

openButton.addEventListener("click", () => {
  if (isOpen) return;

  previousFocus = document.activeElement;
  isOpen = true;
  modal.setAttribute("aria-hidden", "false");
  dialog.focus();
  document.addEventListener("keydown", onKeydown);
});

modal.addEventListener("click", (event) => {
  if (event.target === modal) {
    closeModal();
  }
});

closeButton.addEventListener("click", closeModal);

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

Как избежать конфликтов всплытия событий и вложенных элементов

Проверьте результат по списку. Особенно внимательно тестируйте интерфейсы, где другие компоненты тоже реагируют на клики и клавиши.

  • Клик по тексту, полю, ссылке и кнопке внутри диалога не закрывает окно.
  • Клик по свободной области фона закрывает окно, если такой сценарий разрешён.
  • Нажатие на кнопку закрытия приводит к тому же состоянию, что и остальные способы закрытия.
  • Клики по вложенным элементам не обрабатываются как клик по фону.
  • Обработчик Escape активен только пока окно открыто.
  • После нескольких открытий и закрытий не возникает повторного срабатывания обработчиков.
  • События клавиатуры и клика не закрывают одновременно несколько компонентов.

Не добавляйте без необходимости stopPropagation(): он может нарушить работу других обработчиков на странице. Проверка равенства цели слою обычно решает задачу без подавления всплытия.

Фокус, доступность и корректное возвращение к кнопке открытия

Типичные ошибки, которые ухудшают доступность и делают поведение непредсказуемым:

  • Диалог открывается визуально, но его состояние не отражается в доступной разметке.
  • После открытия фокус остаётся на элементе под затемнением.
  • Пользователь клавиатуры может перейти табуляцией за пределы открытого диалога.
  • При закрытии фокус не возвращается к элементу, открывшему окно.
  • Escape закрывает окно, хотя фокус находится в другом компоненте, который использует эту клавишу.
  • Слой закрывается при клике по дочернему элементу из-за проверки события на всплытии без сравнения цели.
  • Скрытое окно остаётся доступным для клавиатуры или вспомогательных технологий.

Атрибуты role="dialog", aria-modal="true" и связь с заголовком помогают обозначить назначение окна, но сами по себе не реализуют удержание фокуса. Проверьте сценарий с клавишами Tab и Shift+Tab отдельно.

Проверка сценариев: быстрые клики, вложенные окна и повторное открытие

Выбор подхода зависит от типа диалога и того, насколько важна защита от случайного закрытия:

  • Обычный информационный диалог. Закрытие кликом по фону и Escape подходит, если пользователь не теряет введённые данные.
  • Форма с несохранёнными данными. Отключите закрытие по фону или покажите подтверждение перед сбросом введённого.
  • Несколько уровней диалогов. Храните активный верхний диалог и направляйте Escape только ему; не закрывайте все окна одним событием.
  • Нативный элемент dialog. Рассмотрите встроенный HTML-элемент, если подходит его модель открытия и закрытия; проверьте поддержку нужных сценариев в целевых браузерах.

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

Разбор спорных случаев при закрытии модального окна

Почему клик по содержимому иногда закрывает окно?

Обычно обработчик проверяет само наличие клика на внешнем слое, не отличая его от клика по вложенному элементу. Сравнивайте event.target с элементом слоя.

Нужно ли вызывать preventDefault для Escape?

Не всегда. Вызывайте его, только если нужно предотвратить связанное с клавишей действие и это не мешает другим компонентам; сначала проверьте взаимодействие на странице.

Почему Escape не закрывает диалог?

Проверьте, что обработчик keydown установлен при открытии и удаляется только при закрытии, а состояние окна обновляется синхронно.

Можно ли закрывать окно по клику на любой участок фона?

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

Что делать, если открыто несколько модальных окон?

Обычно клавиша Escape должна закрывать только верхний активный диалог. Управляйте порядком окон централизованно, чтобы обработчики не закрывали сразу несколько уровней.

Достаточно ли aria-modal для доступного диалога?

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

Как проверить повторное открытие после закрытия?

Откройте и закройте окно несколько раз, затем проверьте клик по фону, Escape и возврат фокуса. Если одно действие срабатывает несколько раз, ищите повторно добавленные обработчики.

Автор: Алексей Воронов

Scroll to Top