Модальное окно (modal window): что это, отличие от поп-апа и как сделать на dialog

Модальное окно (modal window): что это, отличие от поп-апа и как сделать на dialog Полезное

Модальное окно (modal window) — это окно поверх страницы или приложения, которое блокирует работу с остальным интерфейсом, пока пользователь не ответит или не закроет его. Ключевое слово здесь — «блокирует»: пока окно открыто, кнопки и ссылки под ним не нажимаются.

Ниже — чем модальное окно отличается от поп-апа, полный рабочий пример на HTML-элементе <dialog>, требования доступности и запасной вариант на div для старых браузеров. Примеры проверены 30.09.2026 в Chromium 152 (headless-режим).

Модальное окно, поп-ап и диалог: в чем разница

Эти слова часто используют как синонимы, но они описывают разные свойства.

Термин Что означает Блокирует страницу
Диалог (dialog) Любое окно для разговора с пользователем: вопрос, форма, сообщение Зависит от режима
Модальное окно Диалог в модальном режиме: остальной интерфейс недоступен Да
Немодальное окно Диалог, рядом с которым страницей можно пользоваться (чат поддержки, панель поиска) Нет
Поп-ап (pop-up) Любой всплывающий элемент: баннер подписки, подсказка, меню, новое окно браузера Не обязательно

Отличие поп-апа от модального окна: «поп-ап» говорит о том, как элемент появляется (всплывает), а «модальное» — о том, как он себя ведет (не дает работать с остальным). Рекламный баннер на весь экран — это поп-ап, сделанный модальным. Всплывающая подсказка у иконки — поп-ап, но не модальное окно.

Термин пришел из настольных систем: там модальным называют, например, окно «Сохранить изменения?», которое не дает вернуться к документу, пока не выбран ответ. В Windows так же ведут себя стандартные диалоги «Открыть» и «Сохранить как». Про окна операционной системы — в статье Окно в Windows.

Когда модальное окно уместно

Модальное окно прерывает человека, поэтому оно оправдано, когда без ответа нельзя продолжать:

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

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

Модальное окно на HTML-элементе dialog: полный пример

В HTML есть встроенный элемент <dialog>. Он работает во всех актуальных версиях Chrome, Edge, Firefox и Safari (в Safari — с версии 15.4, в Firefox — с 98, обе вышли в марте 2022 года). Сохраните код в файл modal.html и откройте в браузере.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Модальное окно на dialog</title>
<style>
  dialog {
    border: 0;
    border-radius: 12px;
    padding: 24px;
    max-width: min(90vw, 420px);
  }
  dialog::backdrop {
    background: rgb(0 0 0 / 0.5);
  }
</style>
</head>
<body>
<button id="open">Очистить корзину</button>
<p id="status">В корзине 3 товара</p>

<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Удалить все товары?</h2>
  <p>Это действие нельзя отменить.</p>
  <form method="dialog">
    <button value="cancel" autofocus>Нет</button>
    <button value="ok">Да, удалить</button>
  </form>
</dialog>

<script>
  const dialog = document.getElementById('confirm');
  const openBtn = document.getElementById('open');
  const statusText = document.getElementById('status');

  openBtn.addEventListener('click', () => {
    dialog.returnValue = '';
    dialog.showModal();
  });

  dialog.addEventListener('close', () => {
    if (dialog.returnValue === 'ok') {
      statusText.textContent = 'Корзина пуста';
    }
  });
</script>
</body>
</html>

Что должно получиться: по кнопке «Очистить корзину» страница затемняется, по центру появляется окно, фокус стоит на кнопке «Нет». Кнопка «Да, удалить» закрывает окно и меняет текст на «Корзина пуста». Кнопка «Нет» и клавиша Esc закрывают окно без изменений. После закрытия фокус возвращается на кнопку «Очистить корзину».

Разбор по частям

  • dialog.showModal() открывает окно в модальном режиме: оно попадает в верхний слой поверх любых z-index, а остальная страница перестает реагировать на клики и клавишу Tab.
  • ::backdrop — псевдоэлемент-подложка под окном на весь экран. Он есть только у окна, открытого через showModal().
  • <form method="dialog"> — форма, которая не отправляет данные на сервер, а закрывает окно. Значение value нажатой кнопки попадает в dialog.returnValue.
  • Событие close срабатывает при любом закрытии: кнопкой, клавишей Esc или вызовом dialog.close(). Перед закрытием по Esc дополнительно приходит событие cancel.
  • autofocus ставит начальный фокус на безопасную кнопку «Нет», чтобы случайный Enter не удалил данные.
  • aria-labelledby связывает окно с заголовком: программа экранного доступа объявит «Удалить все товары?» при открытии.

show() и showModal() — не одно и то же

У элемента два метода открытия, и модальным окно делает только один.

Свойство show() showModal()
Страница под окном доступна да нет
Подложка ::backdrop нет есть
Закрытие по Esc нет да
Положение по умолчанию по центру по горизонтали, по вертикали — там, где элемент стоит в документе; прокручивается вместе со страницей по центру экрана, при прокрутке остается на месте

Атрибут open в разметке (<dialog open>) ведет себя как show(): окно видно, но оно немодальное. Поэтому модальное окно открывают из JavaScript.

Ошибка: returnValue остается от прошлого раза

Метод dialog.close() без аргумента закрывает окно, но не меняет returnValue. Если значение не сбросить, в нем останется ответ от предыдущего открытия.

Неверно — окно открывается повторно без сброса:

dialog.showModal();
dialog.close('ok');      // пользователь нажал «Да»
dialog.showModal();      // открыли еще раз
dialog.close();          // закрыли из кода, например по таймеру
console.log(dialog.returnValue);

Фактический результат в консоли:

ok

Обработчик close решит, что пользователь снова подтвердил удаление. Исправление — строка dialog.returnValue = ''; перед showModal(), как в полном примере: тогда после dialog.close() в консоли будет пустая строка.

С клавишей Esc поведение зависит от браузера. Chromium 152 при закрытии по Esc сам записывает в returnValue пустую строку, а в документации MDN описано, что при Esc значение не обновляется. Сброс перед showModal() закрывает оба случая.

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

Клик по затемненной области окно не закрывает. Проще всего включить это атрибутом closedby="any" на элементе <dialog>. Атрибут новый: он есть в Chrome и Edge с версии 134 и в Firefox с версии 141, а в Safari на сентябрь 2026 года в стабильных версиях его нет (по таблицам caniuse). Браузер без поддержки атрибут игнорирует, и окно по-прежнему закрывается только кнопками и Esc, поэтому видимая кнопка закрытия обязательна.

Вторая особенность: showModal() блокирует клики по странице, но не ее прокрутку колесом или пальцем. Если фон не должен двигаться, добавьте правило:

html:has(dialog:modal) {
  overflow: hidden;
}

Доступность: что проверить руками

Элемент <dialog> берет на себя большую часть работы, но не всю. Короткая проверка без мыши:

  1. Открыть окно клавишей Enter на кнопке. Фокус должен оказаться внутри окна.
  2. Нажимать Tab. Фокус ходит по элементам окна и не попадает на ссылки страницы под ним.
  3. Нажать Esc. Окно закрывается, фокус возвращается на кнопку, с которой его открыли.
  4. Убедиться, что у окна есть имя: aria-labelledby с заголовком или aria-label.
  5. Убедиться, что есть видимая кнопка закрытия: Esc нет на телефоне.

Роль dialog и признак модальности браузер выставляет сам, добавлять role="dialog" и aria-modal="true" к элементу <dialog> не нужно. Это упрощенный список: полная проверка включает программу экранного доступа (NVDA, VoiceOver) и контраст текста.

Вариант на div, CSS и JavaScript для старых браузеров

Если нужно поддержать браузеры старше 2022 года или в проекте уже есть самодельные окна, модальное поведение собирают вручную. Все, что <dialog> делает сам, здесь пишется кодом.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Модальное окно на div</title>
<style>
  .overlay {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .overlay[hidden] { display: none; }
  .modal {
    background: #fff;
    border-radius: 12px;
    padding: 24px;
    max-width: 420px;
  }
</style>
</head>
<body>
<button id="open">Очистить корзину</button>
<p id="status">В корзине 3 товара</p>

<div class="overlay" id="overlay" hidden>
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="m-title">
    <h2 id="m-title">Удалить все товары?</h2>
    <button id="no">Нет</button>
    <button id="yes">Да, удалить</button>
  </div>
</div>

<script>
  var overlay = document.getElementById('overlay');
  var modal = overlay.querySelector('.modal');
  var openBtn = document.getElementById('open');
  var noBtn = document.getElementById('no');
  var yesBtn = document.getElementById('yes');

  function openModal() {
    overlay.hidden = false;
    noBtn.focus();
    document.addEventListener('keydown', onKey);
  }

  function closeModal() {
    overlay.hidden = true;
    document.removeEventListener('keydown', onKey);
    openBtn.focus();
  }

  function onKey(event) {
    if (event.key === 'Escape') {
      closeModal();
      return;
    }
    if (event.key !== 'Tab') return;
    var active = document.activeElement;
    if (!modal.contains(active)) {
      event.preventDefault();
      noBtn.focus();
    } else if (event.shiftKey && active === noBtn) {
      event.preventDefault();
      yesBtn.focus();
    } else if (!event.shiftKey && active === yesBtn) {
      event.preventDefault();
      noBtn.focus();
    }
  }

  openBtn.addEventListener('click', openModal);
  noBtn.addEventListener('click', closeModal);
  yesBtn.addEventListener('click', function () {
    document.getElementById('status').textContent = 'Корзина пуста';
    closeModal();
  });
  overlay.addEventListener('click', function (event) {
    if (event.target === overlay) closeModal();
  });
</script>
</body>
</html>

Поведение то же, что в первом примере, плюс закрытие по клику на затемненный фон. Что здесь сделано вручную:

Задача <dialog> + showModal() Вариант на div
Окно поверх страницы верхний слой браузера position: fixed и, при необходимости, z-index
Блокировка кликов по фону встроена подложка .overlay на весь экран
Закрытие по Esc встроено обработчик keydown
Фокус не уходит на страницу встроено ручной перехват Tab и Shift+Tab
Возврат фокуса встроен openBtn.focus()
Роль для экранного доступа встроена role="dialog" и aria-modal="true"

Первая ветка в onKey нужна для случая, когда фокус оказался вне окна: после клика по тексту окна активным элементом становится body, и без этой проверки Shift+Tab уводит фокус на кнопку страницы под затемнением.

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

Если не получилось

  • Окно видно сразу при загрузке. В разметке остался атрибут open или в CSS задано dialog { display: block; }. Закрытый <dialog> скрывает сам браузер, правило display это ломает.
  • Нет затемнения. Окно открыто через show() или атрибутом open. Подложка есть только у showModal().
  • Ошибка при открытии. Вызов showModal() для окна, уже открытого через show(), дает исключение InvalidStateError. Сначала закройте окно методом close().
  • Окно прижато к краю. Стили сбросили margin: auto у dialog. Верните его или не переопределяйте margin.
  • Кнопка в форме перезагружает страницу. У формы нет method="dialog".

Выводы

  • Модальное окно блокирует остальной интерфейс до ответа пользователя; поп-ап — любой всплывающий элемент, он может быть и немодальным.
  • На сайте модальное окно проще всего сделать элементом <dialog> и методом showModal(): подложка ::backdrop, закрытие по Esc, удержание и возврат фокуса работают без дополнительного кода.
  • show() и атрибут open открывают то же окно немодально: без подложки и без блокировки страницы.
  • Перед повторным открытием сбрасывайте returnValue, иначе после close() без аргумента (а в части браузеров и после Esc) в нем останется прошлый ответ.
  • Вариант на div требует вручную написать Esc, работу с фокусом и ARIA-атрибуты; он нужен для старых браузеров и унаследованного кода.

Где применяется / связь с практикой

Модальные окна есть почти в каждом веб-интерфейсе: подтверждение удаления, вход, корзина, просмотр фото. На этой небольшой задаче сходятся три базовые темы фронтенда — разметка, стили и обработка событий, — поэтому ее часто дают как учебное упражнение.

Освойте тему на практике

События, работа с DOM и формами последовательно разбираются на курсе «JavaScript-разработчик. Базовый уровень». Посмотреть формат занятий можно на открытых уроках Otus.

FAQ

Чем <dialog> отличается от атрибута popover?
Элемент с атрибутом popover тоже показывается в верхнем слое, но страницу не блокирует и в режиме по умолчанию (auto) закрывается по клику мимо. Он подходит для меню и подсказок, а для вопроса, требующего ответа, нужен showModal().

Можно ли сделать модальное окно только на CSS, без JavaScript?
Показать и скрыть блок можно через :target или скрытый чекбокс, но Esc, удержание и возврат фокуса так не получить. Такое окно выглядит модальным, но с клавиатуры страница под ним остается доступной.

Чем плохи alert() и confirm()?
Это тоже модальные окна, но их внешний вид и текст кнопок задает браузер, а выполнение скрипта останавливается до ответа. Для интерфейса сайта их заменяют на <dialog>, а alert() оставляют для быстрой отладки.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)