
Почти у каждого фронтендера есть или была своя модалка на div.
Сначала это выглядит просто: оверлей, контейнер, кнопка закрытия — готово. Но очень быстро всплывают детали: закрытие по Escape, клик вне окна, блокировка фона, скролл, слои, z-index, порталы, странные edge‑cases.
Долгое время это было нормой: для любой всплывающей штуки — div, состояние и обработчики. Модалка, dropdown, панель действий, подсказка — всё через один и тот же примитив.
Проблема не в самом div, а в том, что слишком разные задачи годами решали одним и тем же способом. Из-за этого вокруг простого окна быстро нарастал дополнительный код.
В последние годы браузеры стали заметно лучше поддерживать такие вещи прямо из коробки.
<dialog> — это HTML-элемент для окон, в том числе модальных.
Popover API — это уже не про классическую модалку, а про более лёгкие всплывающие элементы: меню, панели действий, небольшие вспомогательные окна, привязанные к кнопке или другому элементу интерфейса.
Здесь же появляется ещё одна важная вещь — top layer. Это отдельный слой поверх страницы, в который браузер помещает такие элементы. Поэтому dialog и popover не ведут себя как обычные блоки в документе, а отображаются поверх остального интерфейса. Именно из-за этого часть старых проблем со слоями и наложением перестаёт быть постоянной ручной болью.
Именно в этом главное изменение подхода. Раньше всплывающее окно почти автоматически означало большой объём одинакового самописного кода. Теперь часть этой механики уже встроена в браузер.
Самый простой пример — обычное подтверждение действия. На div это обычно выглядит примерно так:
<!-- fragment.html -->
<button id="deleteBtn">Удалить проект</button>
<div class="modal" hidden>
<div class="modal-backdrop"></div>
<div class="modal-content" role="dialog" aria-modal="true">
<p>Удалить проект без возможности восстановления?</p>
<button id="cancelBtn">Отмена</button>
<button id="confirmBtn">Удалить</button>
</div>
</div>
/* styles.css */
.modal[hidden] {
display: none;
}
.modal {
position: fixed;
inset: 0;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
.modal-content {
position: relative;
z-index: 1;
width: min(420px, calc(100% - 32px));
margin: 15vh auto 0;
padding: 20px;
border-radius: 16px;
background: #fff;
}
// script.js
const modal = document.querySelector('.modal')
const deleteBtn = document.getElementById('deleteBtn')
const cancelBtn = document.getElementById('cancelBtn')
function openModal() {
modal.hidden = false
document.body.style.overflow = 'hidden'
}
function closeModal() {
modal.hidden = true
document.body.style.overflow = ''
}
deleteBtn.addEventListener('click', openModal)
cancelBtn.addEventListener('click', closeModal)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
Даже в таком коротком примере видно, что простое окно требует много служебного кода. Нужно отдельно показывать и скрывать его, отдельно блокировать скролл, отдельно отслеживать Escape. А если делать всё нормально, дальше добавятся клик вне окна и другие детали.
Теперь посмотрим на тот же сценарий через dialog:
<!-- fragment.html -->
<button id="deleteBtn">Удалить проект</button>
<dialog id="confirmDialog">
<p>Удалить проект без возможности восстановления?</p>
<form method="dialog">
<button value="cancel">Отмена</button>
<button value="confirm">Удалить</button>
</form>
</dialog>
/* styles.css */
#confirmDialog {
width: min(420px, calc(100% - 32px));
padding: 20px;
border: 0;
border-radius: 16px;
}
#confirmDialog::backdrop {
background: rgba(0, 0, 0, 0.45);
}
// script.js
const dialog = document.getElementById('confirmDialog')
const deleteBtn = document.getElementById('deleteBtn')
deleteBtn.addEventListener('click', () => {
dialog.showModal()
})
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'confirm') {
console.log('Удаляем проект')
}
})
Да, в таком маленьком примере разница по объёму кода ещё не кажется большой. Но в реальном проекте она уже становится ощутимой.
На div вам почти всегда приходится отдельно собирать открытие и закрытие окна, затемнение, блокировку скролла, обработку Escape, клик вне окна и другие детали. В примере выше показана только базовая версия, без большей части этих вещей.
С dialog значительная часть этого поведения уже встроена в браузер. Поэтому на простом примере разница выглядит незначительной, а на более сложных окнах становится намного заметнее.
Если нужно немодальное окно, dialog можно открыть без блокировки всей страницы:
settingsDialog.show()
А если нужен именно модальный сценарий с отделением от фона — использовать:
settingsDialog.showModal()
Это как раз тот случай, когда браузер даёт не просто тег, а готовую модель поведения.
Popover хорошо подходит для сценариев, где не нужно открывать отдельное модальное окно, а нужно показать небольшое окно рядом с действием пользователя.
Например, так можно сделать кнопку с быстрыми действиями в карточке.
<!-- fragment.html -->
<button popovertarget="card-actions">Действия</button>
<div id="card-actions" popover>
<button>Переименовать</button>
<button>Дублировать</button>
<button>Архивировать</button>
</div>
/* styles.css */
#card-actions {
padding: 8px;
border: 0;
border-radius: 12px;
}
#card-actions:popover-open {
display: grid;
gap: 8px;
}
В таком примере особенно хорошо видно различие с dialog. Это не отдельный сценарий. Пользователь не перемещается в новое окно. Он просто открывает дополнительное меню рядом с текущим элементом.
Если хотите управлять popover через JavaScript, это тоже делается довольно просто:
const actions = document.getElementById('card-actions')
actions.showPopover()
actions.hidePopover()
actions.togglePopover()
Для dropdown, панели фильтров или небольшого меню действий это обычно лучше, чем тянуть туда модалку или собирать всё на div.
На практике разница становится понятнее, если посмотреть на два коротких сценария.
Подтверждение удаления — это dialog.
<!-- fragment.html -->
<button id="removeBtn">Удалить</button>
<dialog id="removeDialog">
<p>Удалить запись?</p>
<form method="dialog">
<button value="cancel">Нет</button>
<button value="confirm">Да</button>
</form>
</dialog>
Здесь пользователь должен остановиться, принять решение и только потом вернуться к странице.
Меню действий в карточке — это popover.
<!-- fragment.html -->
<button popovertarget="actionsMenu">⋯</button>
<div id="actionsMenu" popover>
<button>Редактировать</button>
<button>Копировать ссылку</button>
</div>
Здесь пользователь не переключается в отдельное окно. Он просто открывает небольшое меню рядом с кнопкой.
Поэтому главный вопрос простой: это отдельное окно, которое останавливает работу со страницей, или небольшое всплывающее меню рядом с текущим элементом?
Если это отдельное окно — чаще нужен dialog. Если это небольшое меню или панель рядом с кнопкой — выбирай Popover API.
Это и есть главное практическое различие между ними. Не размер окна, не его оформление и не количество кнопок. А то, является ли оно отдельным окном или частью текущего интерфейса.
Сам по себе div никуда не делся и, конечно, не стал плохим. Не в этом суть.
Проблема в том, что во фронтенде слишком долго смотрели на модалки, меню и подсказки как на одну и ту же задачу. Нужен слой поверх страницы — значит берём div и дальше дописываем всё остальное сами.
Пока в браузерах не было более точных инструментов, это было нормально. Сейчас ситуация уже другая. Для части задач браузер даёт основу лучше, чем стандартный контейнер с набором обработчиков поверх него.
Это не значит, что нужно срочно переписывать все старые компоненты или объявлять самодельные решения ошибкой. Но это хороший повод перестать тащить старый подход туда, где он уже не нужен.
dialog и Popover API сами по себе не сделают интерфейс хорошим. Они не решают за разработчика, как должно вести себя окно и как всё это встроить в приложение. Но они убирают часть ручной работы там, где её слишком долго считали нормой. И в этом их реальная ценность.