---
title: "Класс управления Модальными окнами"
description: "В прошлый раз я рассказывал про подгрузчик секций, который сам решает, когда тянуть JS и CSS — и, между делом, было упомянуто, про загрузку modalManajer.js.…"
url: "https://rowan-web.ru/blog/klass-upravleniya-modalnymi-oknami/"
date_modified: "2026-08-15T19:40:39+03:00"
language: "ru-RU"
---
В прошлый раз я рассказывал про [подгрузчик секций](https://rowan-web.ru/lenivaya-podgruzka-skriptov-intersectionobserver/), который сам решает, когда тянуть JS и CSS — и, между делом, было упомянуто, про загрузку modalManajer.js. Расскажу отдельно про сам этот файл: как устроено окно, которое одинаково открывается из шапки, из карточки товара и из футера — без отдельного обработчика на каждую кнопку.

Проблема: у каждой модалки — своя открывашка
--------------------------------------------

Естественный путь добавления модалки в тему — найти кнопку по id или классу и повесить на неё click, внутри показать нужный блок. Работает, пока модалка одна. На второй копируется тот же кусок кода с другим id, на третьей — снова. Через полгода в теме десяток почти одинаковых обработчиков, разбросанных по шаблонам, и у каждого — свой список того, что забыли: где-то не убирается блокировка скролла body, где-то не отрабатывает клик по фону, где-то забыли снять старый listener при повторной инициализации.   
Или вполне возможен второй вариант — устанавливаем плагин. Кастомизируем, подстраиваем под себя… и имеем кучу ненужных функций, огромное количество неоптимизированного кода, с блокировкой рендера и т.д..   
Да, важный момент. Я подразумеваю, что вы уже имеете некие базовые понятия и стилизация модалки для вас — это не проблема.

Один делегированный обработчик на весь документ
-----------------------------------------------

Вместо этого — один класс BaseModalManager, один экземпляр на страницу, один click-listener на document. Вместо того чтобы искать конкретную кнопку, обработчик на каждом клике проверяет через closest(), не попали ли по одному из трёх типов элементов: кнопке открытия, кнопке закрытия или самому фону модалки.

```
class BaseModalManager {
  constructor() {
    this.animationDelay = 300;
    this.handleClick = this.handleClick.bind(this);
    this._timer = null;
    this.init();
  }

  init() {
    document.addEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const openButton = event.target.closest('[data-modal-open]');
    if (openButton) {
      event.preventDefault();
      this.openModal(openButton.getAttribute('data-modal-open'));
      return;
    }

    const closeButton = event.target.closest('[data-modal-close]');
    if (closeButton) {
      event.preventDefault();
      this.closeModal(closeButton.closest('[data-modal]'));
      return;
    }

    // клик пришёлся на сам фон модалки, а не на её содержимое — тоже закрытие
    const modal = event.target.closest('[data-modal]');
    if (modal && event.target === modal) {
      this.closeModal(modal);
    }
  }
}
```

event.target.closest() тут делает всю работу: неважно, кликнули по самой кнопке или по иконке/тексту внутри неё — closest() всё равно найдёт ближайший элемент с нужным атрибутом вверх по дереву. И неважно, сколько на странице модалок и кнопок для них — обработчик один, и вешается он один раз при инициализации класса, а не по одному на каждый триггер.

Три атрибута вместо JS в шаблоне
--------------------------------

Чтобы завести новую модалку, в шаблон ничего, кроме разметки, добавлять не нужно. Открывающий элемент получает data-modal-open со значением, которое связывает его с конкретным окном:

```
<button data-modal-open="modal-form">Оставить заявку</button>

<div class="modal" data-modal="modal-form">
  <div class="modal_inner">
    <button data-modal-close>×</button>
    <!-- форма -->
  </div>
</div>
```

Значение data-modal-open и data-modal — это просто общий id, по которому JS находит нужное окно через querySelector. Открывать одну и ту же модалку можно с любого числа мест на странице сразу — из шапки, из карточки услуги, из футера — просто повторив тот же data-modal-open в разметке, без единой новой строчки JS.

Две фазы анимации и защита от быстрого дребезга
-----------------------------------------------

У модалки в CSS два переключаемых класса. Базовое состояние — display:none и opacity:0, transition висит на всех свойствах сразу:

```
.modal {
  display: none;
  opacity: 0;
  transition: all .3s linear;
  position: fixed;
  inset: 0;
}

.modal.active {
  display: flex;
}

.modal.active.animation {
  opacity: 1;
}
```

display у CSS-transition не анимируется — переключить и display, и opacity одним классом в один такт браузер может схлопнуть в один пересчёт стилей и просто дёрнуть окно без всякого фейда. Поэтому классы разнесены по времени: .active выставляет display:flex сразу, а .animation, которая и запускает переход opacity 0 → 1, добавляется отдельно, через таймаут. На закрытии — обратный порядок: сначала снимается .animation, окно начинает растворяться, а .active (и вместе с ним display:none) снимается только после того, как transition должен был завершиться.

```
openModal(modalId) {
  const modal = document.querySelector(`[data-modal="${modalId}"]`);
  if (!modal) return;

  // отменяем отложенную операцию противоположной фазы — иначе быстрые
  // открыл-закрыл-открыл приводят к рассинхрону между active и animation
  clearTimeout(this._timer);

  modal.classList.add('active');
  this._timer = setTimeout(() => {
    modal.classList.add('animation');
  }, this.animationDelay);
}

closeModal(modal) {
  if (!modal) return;

  clearTimeout(this._timer);

  modal.classList.remove('animation');
  this._timer = setTimeout(() => {
    modal.classList.remove('active');
  }, this.animationDelay);
}
```

clearTimeout в начале обоих методов — не для красоты. Без него быстрый клик «открыть» и почти сразу «закрыть» (или просто дважды подряд по той же кнопке) оставляет висеть таймер от предыдущего вызова, который потом снимает нужный класс не в той фазе — окно либо остаётся видимым без анимации, либо пропадает раньше времени. Один \_timer на экземпляр и его сброс перед каждым новым переключением фазы убирает этот класс багов целиком.

Инициализация
-------------

Единственный глобальный экземпляр:

```

  window.BaseManager = new BaseModalManager();

```

Инициализировали, все. Дальше раскидывайте управление и добавляйте разметку модалки. Никаких дописываний новых не нужно.   
Важный момент. Что данный класс также покрывает. Смоделируем ситуацию. У вас есть ajax подгрузка карточки, которая в свою очередь, вызывает модальное окно. Как думаете, после добавления в разметку нового кода, вызовется ли модалка? Ведь на момент инициализии класса в разметке не было этого самого вызова….   
Ответ — да. Она вызовется. Почему? Потому что bind…. Все это уже предусмотрено.

Итого, что мы имеем
-------------------

- Новая модалка — это разметка и три data-атрибута, без единой строчки JS в шаблоне.
- Один click-listener на документ вместо десятка обработчиков на конкретные кнопки — меньше кода, меньше мест, где можно забыть отписаться.
- Одну и ту же модалку можно открывать с любого числа мест на странице — id в data-modal-open просто совпадает с id в data-modal.
- Таймер на каждую фазу анимации и его сброс перед новым переключением закрывают целый класс багов с быстрым дребезгом открыл/закрыл.

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

PS: Веб разработка — уникальная штука. Добиться одной и тойже цели можно абсолютно разными способами, и нельзя сказать, что какой-то из них будет неправильным. Суть статьи — донести мысль. Тонкости реализации — это уже дело каждого….

[Полный список страниц сайта, читаемых AI](https://rowan-web.ru/llms.txt)
