Обзор
Используйте нативный элемент <dialog> для модальных и немодальных диалоговых окон. Он обеспечивает семантику специальных возможностей, обработку фокуса, рендеринг верхнего слоя и обработку Esc, которые пользовательские оверлеи должны перестраивать вручную.
Выберите тип оверлея
Выбрать первое подходящее правило. Если вы не уверены, используйте правило 1.
Правило 1. Оверлей прерывает действия пользователя и блокирует страницу (по умолчанию)
Используйте для подтверждений, форм, входа, деструктивных действий и любых модальных, диалоговых, лайтбокс- или всплывающих запросов, если не указано иное.
Используйте <dialog>, открытый как модальное окно, декларативно с command="show-modal" (см. Декларативное управление диалоговыми и всплывающими окнами) или из скрипта с showModal().
<button type="button" command="show-modal" commandfor="confirm">Delete</button>
<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
Браузер предоставляет: инертную страницу, ограничение фокуса, верхний слой, ::backdrop, Esc и закрытие жестом назад, а также возврат фокуса к вызывающему объекту.
- НЕ добавляйте
role="dialog",aria-modal, ловушку фокуса, переключениеinertили обработчикEsc. - НЕ открывайте его с помощью тега
show()или атрибутаopen. - РЕКОМЕНДУЕТСЯ останавливать прокрутку страницы за объявлением с помощью приведенного ниже кода CSS. Подробнее о том, как запретить прокрутку страницы под модальным диалоговым окном…
- НЕ блокируйте прокрутку с помощью
overflow: hiddenилиposition: fixedвhtmlилиbody.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
overscroll-behavior: contain;
}
Не используйте для меню и подсказок (см. правило 3).
Правило 2. Пользователь продолжает работать на странице, пока панель открыта
Используйте для панелей поиска, инспекторов, палитр и панелей создания, которые семантически являются диалоговыми окнами.
Используйте <dialog popover> для панели верхнего слоя с закрытием всплывающего окна (auto закрывается при нажатии за пределами окна, manual – нет). Используйте dialog.show(), только если панель не должна находиться на верхнем слое.
<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
<h2 id="find-title">Find</h2>
<input type="search" aria-label="Search">
<button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>
Страница остается интерактивной и не становится инертной.
- РЕКОМЕНДУЕТСЯ добавлять видимую кнопку закрытия.
- НЕ используйте
showModal(), который блокирует страницу.
Не используйте для подтверждения блокировки (см. правило 1).
Правило 3. Оверлей не является диалоговым окном
Используется для меню, раскрывающихся списков, подсказок, всплывающих карточек и всплывающих уведомлений.
Используйте семантический элемент с атрибутом popover, а не <dialog>.
<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
<li><a href="/rename">Rename</a></li>
</ul>
Выберите, как можно закрыть баннер
Значение по умолчанию зависит от того, как открыто диалоговое окно. Если правило closedby не задано или задано неверно, то showModal() работает как closerequest, а любой другой способ открытия – как none.
- Только клавиша Esc и жест "Назад" (по умолчанию для модальных окон): ничего не задавайте или укажите
closedby="closerequest". - Закрытие при нажатии за пределами окна. Используйте
closedby="any". - Закрыть может только разработчик (например, форму с несохраненными изменениями). Используйте
closedby="none"и добавьте подтверждение. См. раздел "Закрытие" ниже.
Правила
Доступное название и начальный фокус
- РЕКОМЕНДУЕТСЯ присваивать название каждому диалоговому окну с помощью атрибута
aria-labelledby, указывающего на заголовок, илиaria-label, если видимого заголовка нет. - РЕКОМЕНДУЕТСЯ устанавливать атрибут
autofocusдля основного элемента управления или первого поля. - РЕКОМЕНДУЕТСЯ использовать
autofocusдля безопасного действия (например, "Отмена" или "Сохранить") в подтверждении необратимого действия. - НЕ пишите код, который возвращает фокус при закрытии. Браузер возвращает его вызывающей стороне.
От центра
- РЕКОМЕНДУЕТСЯ открывать декларативно с помощью
command="show-modal"иcommandforв<button>. Подробнее о декларативном управлении диалоговыми и всплывающими окнами… - РЕКОМЕНДУЕТСЯ использовать
dialog.showModal()при открытии из скрипта.
Заключение
- РЕКОМЕНДУЕТСЯ закрывать объявление с помощью кнопок
<form method="dialog">илиcommand="close". - РЕКОМЕНДУЕТСЯ тщательно выбирать
closedby:any(закрытие при нажатии за пределами окна иEsc),closerequest(толькоEsc, по умолчанию для модальных окон) илиnone(только закрытие, управляемое разработчиком). - ИСПОЛЬЗУЙТЕ
command="request-close"илиrequestClose()для кнопок отмены, которые должны учитывать логику несохраненных изменений. Сначала срабатываетcancel, аclosedbyигнорируется. - РЕКОМЕНДУЕТСЯ использовать
closedbyкак прогрессивное улучшение.Escи жест "Назад" на мобильных устройствах уже закрывают модальные окна.
Зацепление за крюк
- РЕКОМЕНДУЕТСЯ считывать
dialog.returnValueв прослушивателеclose, чтобы узнать, какая кнопка была нажата, а затем сбрасывать ее с помощьюdialog.returnValue = ''.Escи другие подобные запросы не задают значение, а браузеры по-разному обрабатывают очистку предыдущего значения, поэтому без сброса может произойти утечка устаревшего значения. Считать, что пользователь'', если он закрыл окно, не выбрав ни один из вариантов. - РЕКОМЕНДУЕТСЯ использовать
cancelдля перехватаEscиrequest-close, но не полагаться наpreventDefault()для повторных нажатийEscбез активации пользователем. - РЕКОМЕНДУЕТСЯ использовать
closedby="none"с явным шагом подтверждения для форм с несохраненными изменениями. - РЕКОМЕНДУЕТСЯ сообщать пользователю, почему диалоговое окно осталось открытым, если действие
cancelбыло заблокировано. Для этого покажите видимое сообщение и сошлитесь на него с помощью атрибутаaria-describedbyиз элемента управления, который получает фокус (например, "Отменить"), чтобы программа чтения с экрана прочитала его при перемещении фокуса. Без него при переходе к новой кнопке будет озвучиваться только ее название. - НЕ допускайте, чтобы элементы управления перемещались под указатель. Зарезервируйте место для сообщения (скройте его с помощью
visibility, а неdisplayилиhidden) и добавьте кнопку удаления так, чтобы существующие кнопки не перемещались (например, первой в строке с выравниванием по правому краю). Тогда второй клик в том же месте не приведет к отмене изменений.
Немодальные диалоговые окна
- РЕКОМЕНДУЕТСЯ использовать
<dialog popover>(autoилиmanual) для панели верхнего слоя иdialog.show(), если верхний слой не нужен. - РЕКОМЕНДУЕТСЯ добавлять видимую кнопку закрытия в каждое немодальное диалоговое окно.
Фон и стиль
- РЕКОМЕНДУЕТСЯ настраивать стиль
::backdropи использоватьdialog:modalдля таргетинга стилей, предназначенных только для модальных окон. - РЕКОМЕНДУЕТСЯ использовать анимацию для анимирования элементов верхнего слоя и элементов, перемещаемых на него, перетаскивание – для создания перетаскиваемого элемента, а смену родительского элемента – для создания постоянного интерфейса верхнего слоя.
- НЕ устанавливайте для
displayзначениеdialogбез условий. Оно переопределяет статус "Закрыто". Задайте область действия с помощьюdialog[open].
Специальные возможности
- НЕ ДОБАВЛЯЙТЕ
role="dialog",aria-modal, ловушку фокуса или переключательinertв нативное модальное окно. - РЕКОМЕНДУЕТСЯ использовать стандартный элемент
<button>с доступным названием для элементов управления закрытием. - РЕКОМЕНДУЕТСЯ использовать один уровень заголовков для названий диалоговых окон.
Резервные стратегии
closedby (dialog-closedby)
Без closedby модальное окно по-прежнему закрывается с помощью Esc и жеста "Назад". Не работает только нажатие за пределами окна, поэтому это постепенное сокращение. closedby="none" игнорируется аналогичным образом: Esc запускает cancel, а затем закрывает диалоговое окно. Добавьте защиту от несохраненных изменений в обработчик cancel, а не только в closedby="none", чтобы она также работала, когда closedby отсутствует. Если целевой версии Baseline требуется закрытие по клику вне области, добавьте этот код там, где отсутствует closedby:
if (!('closedBy' in HTMLDialogElement.prototype)) {
// Test the pointer position, not event.target: the <dialog> is also the target for its own
// padding and the gaps between its children, so a target check would close on those clicks
const outside = (e, dialog) => {
const r = dialog.getBoundingClientRect();
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
};
for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
let pressedOutside = false;
dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
// Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
}
}
overscroll-behavior
Рассматривайте ограничение прокрутки как прогрессивное улучшение. Не используйте определение функций: @supports сообщает, что свойство поддерживается, даже если контейнеры без прокрутки его игнорируют. Если это не применяется, диалоговое окно по-прежнему содержит прокрутку, когда его собственное содержимое переполняется, а showModal() по-прежнему делает страницу инертной. Не используйте overflow: hidden или position: fixed в html или body, так как это удаляет корневую полосу прокрутки и сдвигает макет.
Команды Invoker (command/commandfor)
Если целевой Baseline не включает команды Invoker, сохраните ту же разметку и условно загрузите полифил (то же самое, что и прогрессивное улучшение). Определяйте наличие интерфейса и никогда не загружайте его безусловно:
// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill'); // npm install invokers-polyfill
}
Только правила 2 и 3 используют всплывающее окно.
Резервные варианты и поддержка Popover в браузерах
Popover API в основном представляет собой прогрессивное улучшение, но его определяющие характеристики – продвижение верхнего слоя, легкое отклонение и вызов popovertarget – не имеют эквивалента только для CSS. Для старых браузеров нужен полифил или резервный вариант, если вы не хотите его использовать.
Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.
С помощью сборщика или карты импорта:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Если вы не используете сборщик, импортируйте из CDN в <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.