Кратко
СкопированоТег создаёт всплывающее окно или диалог. По умолчанию не показывается на странице.
Может открываться в двух режимах:
- Всплывающее окно — не блокирует взаимодействие со страницей.
- Модальное окно — откроется поверх страницы, имеет фоновое затемнение, остальной контент не доступен для взаимодействия.
Как пишется
СкопированоПарный тег <dialog><
, внутри которого находится содержимое всплывающего окна. У <dialog>
нельзя использовать атрибут tabindex
.
<dialog> Привет, мир!</dialog>
<dialog> Привет, мир! </dialog>
Также у модального окна обязательно должно быть имя — его краткое название. Благодаря этому пользователи вспомогательных технологий знают, что это за элемент и какое у него содержимое.
Имя окну можно добавить двумя способами:
aria
добавляет имя, о котором знают только пользователи скринридеров.
<dialog aria-label="Приветствие"> Привет, мир!</dialog>
<dialog aria-label="Приветствие"> Привет, мир! </dialog>
aria
связывает <dialog>
с видимым всем именем.
<dialog aria-labelledby="dialog-name"> <h3 id="dialog-name"> Привет, мир! </h3> <p>Вы не ждали, а вот он я.</p></dialog>
<dialog aria-labelledby="dialog-name"> <h3 id="dialog-name"> Привет, мир! </h3> <p>Вы не ждали, а вот он я.</p> </dialog>
Как открыть
СкопированоКак и у элемента <details>
, по умолчанию содержимое окна скрыто от пользователя, но его можно отобразить через атрибут open
.
<dialog open> Я виден. Привет! 👋</dialog><dialog> Я скрыт от пользователя 🥷</dialog>
<dialog open> Я виден. Привет! 👋 </dialog> <dialog> Я скрыт от пользователя 🥷 </dialog>
Также окно можно открыть с помощью JavaScript-методов:
show
— добавляет атрибуты( ) open
иaria
.- modal = "false" show
— открывает в режиме «модального окна». Добавляет атрибутыModal ( ) open
иaria
. Появляется подложка в виде псевдоэлемента- modal = "true" :
, который можно стилизовать.: backdrop
<button type="button" onclick="window.myDialog.show()"> Просто открыть</button><button type="button" onclick="window.myDialog.showModal()"> Открыть как модалку</button><dialog id="myDialog">🖖 Живи долго и процветай!</dialog>
<button type="button" onclick="window.myDialog.show()"> Просто открыть </button> <button type="button" onclick="window.myDialog.showModal()"> Открыть как модалку </button> <dialog id="myDialog">🖖 Живи долго и процветай!</dialog>
Как закрыть
Скопировано- Из JavaScript с помощью метода
close
.( ) - Из HTML по событию
submit
(например по нажатию кнопки<button type
), если в= "submit"> <dialog>
есть тег<form>
с атрибутомmethod
.= "dialog"
<dialog open="open" id="closeMe" aria-labelledby="heading"> <h2 id="heading">Закрой меня! 🙏</h2> <p>Результат этих кнопок одинаковый.</p> <button type="button" onclick="window.closeMe.close()"> Закрыть с помощью JavaScript </button> <form method="dialog"> <!-- Если у тега <button> не указан type, то по-умолчанию он будет type="submit" ! --> <button> Закрыть с помощью формы </button> </form></dialog>
<dialog open="open" id="closeMe" aria-labelledby="heading" > <h2 id="heading">Закрой меня! 🙏</h2> <p>Результат этих кнопок одинаковый.</p> <button type="button" onclick="window.closeMe.close()"> Закрыть с помощью JavaScript </button> <form method="dialog"> <!-- Если у тега <button> не указан type, то по-умолчанию он будет type="submit" ! --> <button> Закрыть с помощью формы </button> </form> </dialog>
Возвращаемое значение
СкопированоЕсли кнопкам в форме задать value
, то при закрытии диалога это значение будет присваиваться dialog
.
Присвоим двум кнопкам разные значения:
<form class="options" method="dialog"> <button class="button button--dark" value="debug"> Дави его! </button> <button class="button button--light" value="reproduction"> Каждая жизнь священна </button></form>
<form class="options" method="dialog"> <button class="button button--dark" value="debug"> Дави его! </button> <button class="button button--light" value="reproduction"> Каждая жизнь священна </button> </form>
Если всплывающее окно закрыто по кнопке Дави его!, то количество 🐞 уменьшается. А если по кнопке Каждая жизнь священна, то увеличивается:
if (dialog.returnValue === "debug") { bugs.innerText = bugs.innerText.substring(0, bugs.innerText.length - 2)} else { bugs.innerText += "🐞"}
if (dialog.returnValue === "debug") { bugs.innerText = bugs.innerText.substring(0, bugs.innerText.length - 2) } else { bugs.innerText += "🐞" }
Как понять
СкопированоДолгое время в HTML не существовало тега для создания всплывающих окон. Если такая задача возникала, то использовались либо самописные решения для красивых попапов, либо JavaScript-методы alert
, prompt
и confirm
, если красота была не важна.
Тег <dialog>
появился как альтернатива. Хорошее диалоговое окно — это не просто логика «Показать» и «Скрыть». В <dialog>
реализовано то, о чём часто забывают:
- Для вспомогательных технологий
<dialog>
— аналогrole
. Если окно открыто в режиме модального, то и аналог= "dialog" aria
. Также у тега есть- modal = "true" aria
, поэтому скринридеры сразу же зачитывают его содержимое.- live = "assertive" - Модальные диалоги закрываются по нажатию на Esc.
- У модального диалога при открытии появляется «ловушка фокуса»: для клавиатурной навигации доступны только интерактивные элементы только текущего диалога.
- Браузер запоминает какой элемент был в фокусе до открытия окна и после закрытия окна снова переводит его в фокус.
Вся это логика реализована в самом браузере «из коробки». А значит пользователю не отправляется лишний трафик.
Подсказки
Скопировано💡 Google Chrome при закрытии модального окна клавишей Esc ставит предыдущий элемент не просто в :focus
, а в :focus
. Подразумевая, что пользователь перешёл на клавиатурную навигацию.
💡 По нажатию Esc сначала запускается событие cancel
, а затем close
. Это может быть полезно, если мы хотим отгородить пользователя от случайного нажатия клавиши, сначала предупредив, что изменённые данные не сохранятся, и только при повторном нажатии закрывать окно.
💡 Контент <dialog>
по умолчанию скрыт с помощью display
. Можно переписать это поведение в стилях и анимировать открытие и закрытие. Намного легче, чем аналогичная задача в <details>
например.
💡 Модальные окна «ускользают» от контекста: даже если в HTML-разметке после модального окна указан тег <div>
с z
, то модальное окно всё равно отобразится поверх этого <div>
. Или если родитель наклонён с помощью skew
, то дочернее модальное окно всё равно откроется без наклона.
На практике
Скопированосоветует Скопировано
Блокируем скролл
СкопированоНесмотря на то, что модальное окно перекрывает весь остальной контент на странице с помощью псевдоэлемента :
, вся остальная страница всё равно доступна для прокрутки. Это может смущать пользователя, если на заднем плане будет что-то мельтешить.
Решить эту проблему можно, ставя overflow
на <body>
. В демке ниже это реализовано добавлением класса scroll
.
Так же с помощью scrollbar
можно «зарезервировать» место под скролл, чтобы контент не прыгал при его исчезновении скроллбара.
html,body { scrollbar-gutter: stable;}
html, body { scrollbar-gutter: stable; }
Не забываем так же вернуть всё как было, при закрытии.
dialogOpener.addEventListener("click", openModalAndLockScroll)dialog.addEventListener("close", returnScroll)function openModalAndLockScroll() { dialog.showModal() document.body.classList.add("scroll-lock")}function returnScroll() { document.body.classList.remove("scroll-lock")}
dialogOpener.addEventListener("click", openModalAndLockScroll) dialog.addEventListener("close", returnScroll) function openModalAndLockScroll() { dialog.showModal() document.body.classList.add("scroll-lock") } function returnScroll() { document.body.classList.remove("scroll-lock") }
Закрываем по клику на ::backdrop
СкопированоЧастый UX-сценарий, что модальное окно закрывается по клику на подложку (оверлей). Поскольку для <dialog>
подложкой является псевдоэлемент :
, то просто навесить на него обработчик клика не выйдет.
Однако клик по :
считается и кликом по самому элементу <dialog>
. Значит можно обернуть весь контент модального окна в обёртку и отлавливать когда клик проходит по самому диалогу, а когда по контенту в нём.
<dialog class="dialog"> <div class="dialog__wrapper"> Содержимое диалога </div></dialog>
<dialog class="dialog"> <div class="dialog__wrapper"> Содержимое диалога </div> </dialog>
У элемента диалога есть стандартные браузерные отступы и обводка. А значит их нужно обнулить и поставить на обёртку, чтобы она перекрывала всю «полезную область окна». Иначе клики по отступам тоже будут закрывать модальное окно.
.dialog { border: none; padding: 0;}.dialog__wrapper { padding: 1em;}
.dialog { border: none; padding: 0; } .dialog__wrapper { padding: 1em; }
Теперь на элемент диалога мы можем добавить обработчик клика. Если пользователь нажал на подложку, то current
будет совпадать с target
. В противном случае, клик пошёл на дочерний DOM-узел, который и будет target
.
dialogElement.addEventListener("click", closeOnBackDropClick)function closeOnBackDropClick({ currentTarget, target }) { const dialogElement = currentTarget const isClickedOnBackDrop = target === dialogElement if (isClickedOnBackDrop) { dialogElement.close() }}
dialogElement.addEventListener("click", closeOnBackDropClick) function closeOnBackDropClick({ currentTarget, target }) { const dialogElement = currentTarget const isClickedOnBackDrop = target === dialogElement if (isClickedOnBackDrop) { dialogElement.close() } }
⚠️ Помните, что клик по подложке это вспомогательный способ закрытия. Если ваш дизайнер не нарисовал явный элемент для закрытия, то убедите его это сделать. Ну или убедите себя, если вы сам дизайнер.
Закрываем диалог по клику по свободной области
СкопированоЭтот пример похож на предыдущий, только теперь по отслеживаем клики по всему документу и проверяем был ли кликнут диалог или его потомок. Если оба случая неверны, значит клик прошёл вне диалога и его можно закрыть.
function closeDialogOnOutsideClick({ target }) { const isClickOnDialog = target === dialogElement const isClickOnDialogChildrenNodes = dialogElement.contains(target) const isClickOutsideOfDialog = !( isClickOnDialog || isClickOnDialogChildrenNodes ) if (isClickOutsideOfDialog) { dialogElement.close() }}
function closeDialogOnOutsideClick({ target }) { const isClickOnDialog = target === dialogElement const isClickOnDialogChildrenNodes = dialogElement.contains(target) const isClickOutsideOfDialog = !( isClickOnDialog || isClickOnDialogChildrenNodes ) if (isClickOutsideOfDialog) { dialogElement.close() } }
Расширяем браузерную поддержку
СкопированоПо данным Can I Use, Firefox и Safari начали поддерживать <dialog>
только в марте 2022 года. Для продакшена большинства проектов, по крайней мере ближайшие несколько лет, нужно поддерживать и более старые версии браузеров. Что делать? Отказываться от такого удобного элемента?
К счастью, команда Google Chrome давно разработала полифил, который имитирует работу <dialog>
в старых браузерах. Всё что нужно это подключить скрипт и дополнительные стили.
Но стойте! Неужели ≈3/4 наших пользователей придётся грузить скрипт, который им вообще не нужен? Получается, одно из главных преимуществ нативных диалоговых окон сразу отпадает. А если из-за полифила эти нативные окна будут работать нестабильно?
К счастью, этих проблем можно избежать с помощью динамического импорта:
/** * В реальных проектах мы бы брали полифил из Node пакета. * Но для примера воспользуемся CDN**/const dialogPolyfillURL = "https://esm.run/dialog-polyfill"const isBrowserNotSupportDialog = window.HTMLDialogElement === undefined/** * Подключаем полифил к каждому dialog на странице, * если в браузере нет поддержки**/if (isBrowserNotSupportDialog) { const dialogs = document.querySelectorAll("dialog") dialogs.forEach(async (dialog) => { const { default: polyfill } = await import(dialogPolyfillURL) polyfill.registerDialog(dialog) })}
/** * В реальных проектах мы бы брали полифил из Node пакета. * Но для примера воспользуемся CDN **/ const dialogPolyfillURL = "https://esm.run/dialog-polyfill" const isBrowserNotSupportDialog = window.HTMLDialogElement === undefined /** * Подключаем полифил к каждому dialog на странице, * если в браузере нет поддержки **/ if (isBrowserNotSupportDialog) { const dialogs = document.querySelectorAll("dialog") dialogs.forEach(async (dialog) => { const { default: polyfill } = await import(dialogPolyfillURL) polyfill.registerDialog(dialog) }) }
Помимо скрипта нужно написать и стили. Вы можете, как просто взять из того же репозитория с полифилом, либо сразу адаптировать под себя.
Обратите внимание, что скрипт полифила не может создать псевдоэлемент :
, поэтому стили для него вам нужно дублировать и для <div>
с классом .backdrop
.
dialog::backdrop { background-color: rgb(0 0 0 / 70%);}dialog + .backdrop { background-color: rgb(0 0 0 / 70%);}
dialog::backdrop { background-color: rgb(0 0 0 / 70%); } dialog + .backdrop { background-color: rgb(0 0 0 / 70%); }