Popover API или Dialog API: что выбрать?
- 14 марта 2026
После продолжительного изучения темы выяснилось, что Popover API и Dialog API кардинально отличаются с точки зрения доступности. Если вы стоите перед выбором, придерживайтесь такого правила:
- Используйте Popover API для большинства поповеров.
- Используйте Dialog API только для модальных диалогов.
Поповеры и диалоги
Взаимосвязь между поповерами и диалогами кажется запутанной, но на деле всё просто.
Диалоги — это просто подмножество поповеров. А модальные диалоги — подмножество диалогов. Именно поэтому Popover API можно применять даже к элементу <dialog>:
<!-- Popover на элементе dialog -->
<dialog popover>...</dialog>Визуальное различие между поповерами и модальными окнами тоже весьма наглядно:
- Модальные окна должны отображать подложку (
::backdrop). - Поповеры — нет.
Поэтому никогда не стилизуйте ::backdrop у поповера.
Это визуально превратит поповер в диалог, что влечёт целый ворох проблем.
Стилизуйте ::backdrop только у модального окна.
Popover API и доступность
Создать поповер с помощью Popover API несложно. Нужно указать три вещи:
- атрибут
popovertargetна триггере, - атрибут
idна самом поповере, - атрибут
popoverна элементе поповера.
Значение popovertarget должно совпадать с id.
<button popovertarget="the-popover"> ... </button>
<dialog popover id="the-popover"> Содержимое поповера </dialog>
Обратите внимание: здесь используется элемент <dialog>, чтобы задать роль dialog. Это необязательно, но рекомендуется, поскольку большинство поповеров по сути и являются диалогами.
Эти две строки кода дают в нагрузку целый набор встроенных функций доступности:
- Автоматическое управление фокусом — фокус перемещается на поповер при открытии и возвращается на триггер при закрытии.
- Автоматическая ARIA-связь — браузер сам управляет атрибутами
aria-expanded,aria-popupиaria-controls. - Автоматическое «лёгкое закрытие» — поповер закрывается при клике за его пределами или нажатии клавиши Esc.
Dialog API и доступность
В отличие от Popover API, Dialog API по умолчанию лишён многих встроенных функций:
- Нет автоматического управления фокусом.
- Нет автоматической ARIA-связи.
- Нет автоматического «лёгкого закрытия».
Всё это придётся реализовывать вручную с помощью JavaScript. Именно поэтому Popover API превосходит Dialog API почти во всём — за одним исключением: когда речь идёт о модальных окнах.
Dialog API предоставляет метод showModal(). При его вызове браузер:
- автоматически делает остальные элементы инертными (
inert), - запрещает переключение фокуса на другие элементы по Tab,
- скрывает остальные элементы от программ экранного доступа.
Это работает настолько эффективно, что ловушка фокуса внутри модального окна больше не нужна.
Тем не менее управление фокусом и ARIA-атрибутами при использовании Dialog API придётся брать на себя. Вот минимальный HTML-каркас для функционирующего диалога:
<button
class="modal-invoker"
data-target="the-modal"
aria-haspopup="dialog"
>...</button>
<dialog id="the-modal">Содержимое модального окна</dialog>
Обратите внимание: атрибут aria-expanded намеренно не добавлен в HTML. Причины таковы: это снижает сложность разметки; атрибуты aria-expanded, aria-controls и управление фокусом всё равно требуют JavaScript — логично перенести их туда; такая разметка легче переиспользуется.
Инициализация
Сначала перебираем все элементы .modal-invoker и устанавливаем начальное состояние: aria-expanded="false" и aria-controls, указывающий на нужный диалог.
const modalInvokers = Array.from(document.querySelectorAll('.modal-invoker'))
modalInvokers.forEach(invoker => {
const dialogId = invoker.dataset.target
const dialog = document.querySelector(`#${dialogId}`)
invoker.setAttribute('aria-expanded', false)
invoker.setAttribute('aria-controls', dialogId)
})Открытие модального окна
По клику на триггере меняем aria-expanded на true и вызываем showModal():
modalInvokers.forEach(invoker => {
// ...
invoker.addEventListener('click', event => {
invoker.setAttribute('aria-expanded', true)
dialog.showModal()
})
})Закрытие модального окна
По умолчанию showModal() не поддерживает «лёгкое закрытие», поэтому необходимо добавить кнопку закрытия внутри диалога:
<dialog id="the-modal">
<button class="modal-closer">✕</button>
<!-- Остальное содержимое -->
</dialog>По клику на кнопке закрытия нужно: вернуть aria-expanded="false" на триггер, закрыть диалог методом close() и вернуть фокус на триггер:
const modalClosers = Array.from(document.querySelectorAll('.modal-closer'))
modalClosers.forEach(closer => {
const dialog = closer.closest('dialog')
const dialogId = dialog.id
const invoker = document.querySelector(`[data-target="${dialogId}"]`)
closer.addEventListener('click', event => {
dialog.close()
invoker.setAttribute('aria-expanded', false)
invoker.focus()
})
})Можно ли создать модальное окно с помощью Popover API?
Да, можно. Но тогда придётся самостоятельно реализовать:
- инертность остальных элементов,
- ловушку фокуса.
Установка aria-expanded, aria-controls и управление фокусом, которые рассматривались выше, — значительно проще, чем ручная реализация инертности и ловушки фокуса.
Dialog API может стать намного удобнее в будущем
Существует предложение по командам-инициаторам (invoker commands), благодаря которому Dialog API сможет поддерживать декларативное управление аналогично Popover API. Пока стандарт не принят, придётся вручную реализовывать всё необходимое для доступности.
Погружение в детали: профессиональные поповеры и модальные окна
Рассмотренные примеры — лишь скелет: функциональный и доступный, но пока далёкий от продакшн-качества. В следующих статьях планируется детально разобрать реализацию полноценных поповеров и модальных окон.
А пока — надеемся, что этот материал помог разобраться, когда выбирать Popover API, а когда — Dialog API. Помните: не нужно использовать оба. Чаще всего достаточно одного.
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.