<dialog>

Тег для создания всплывающего окна без боли и страданий.

Время чтения: 5 мин

Кратко

Скопировано

Тег создаёт всплывающее окно или диалог. По умолчанию не показывается на странице.

Может открываться в двух режимах:

  1. Всплывающее окно — не блокирует взаимодействие со страницей.
  2. Модальное окно — откроется поверх страницы, имеет фоновое затемнение, остальной контент не доступен для взаимодействия.

Как пишется

Скопировано

Парный тег <dialog></dialog>, внутри которого находится содержимое всплывающего окна. У <dialog> нельзя использовать атрибут tabindex.

        
          
          <dialog>  Привет, мир!</dialog>
          <dialog>
  Привет, мир!
</dialog>

        
        
          
        
      

Как открыть

Скопировано

Как и у элемента <details>, по умолчанию содержимое окна скрыто от пользователя, но его можно отобразить через атрибут open.

        
          
          <dialog open>  Я виден. Привет! 👋</dialog><dialog>  Я скрыт от пользователя 🥷</dialog>
          <dialog open>
  Я виден. Привет! 👋
</dialog>

<dialog>
  Я скрыт от пользователя 🥷
</dialog>

        
        
          
        
      

Также окно можно открыть с помощью JavaScript-методов:

  1. show() — добавляет атрибуты open и aria-modal="false".
  2. showModal() — открывает в режиме «модального окна». Добавляет атрибуты 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">  <h2>Закрой меня! 🙏</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">
  <h2>Закрой меня! 🙏</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.returnValue.

Присвоим двум кнопкам разные значения:

        
          
          <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-visible. Подразумевая, что пользователь перешёл на клавиатурную навигацию.

💡 По нажатию Esc сначала запускается событие cancel, а затем close. Это может быть полезно, если мы хотим отгородить пользователя от случайного нажатия клавиши, сначала предупредив, что изменённые данные не сохранятся, и только при повторном нажатии закрывать окно.

💡 Контент <dialog> по умолчанию скрыт с помощью display: none. Можно переписать это поведение в стилях и анимировать открытие и закрытие. Намного легче, чем аналогичная задача в <details> например.

💡 Модальные окна «ускользают» от контекста: даже если в HTML-разметке после модального окна указан тег <div> с z-index: 99999, то модальное окно всё равно отобразится поверх этого <div>. Или если родитель наклонён с помощью skew(), то дочернее модальное окно всё равно откроется без наклона.

На практике

Скопировано

Артур Бэйлис Ли советует

Скопировано

Блокируем скролл

Скопировано

Несмотря на то, что модального окно перекрывает весь остальной контент на странице с помощью псевдоэлемента ::backdrop, вся остальная страница всё равно доступна для прокрутки. Это может смущать пользователя, если на заднем плане будет что-то мельтешить.

Решить эту проблему можно, ставя overflow: hidden на <body>. В демке ниже это реализовано добавлением класса scroll-lock.

Так же с помощью scrollbar-gutter можно «зарезервировать» место под скролл, чтобы контент не прыгал при его исчезновении скроллбара.

        
          
          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> подложкой является псевдоэлемент ::backdrop, то просто навесить на него обработчик клика не выйдет.

Однако клик по ::backdrop считается и кликом по самому элементу <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;
}

        
        
          
        
      

Теперь на элемент диалога мы можем добавить обработчик клика. Если пользователь нажал на подложку, то currentTarget будет совпадать с 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)
  })
}

        
        
          
        
      

Помимо скрипта нужно написать и стили. Вы можете, как просто взять из того же репозитория с полифилом, либо сразу адаптировать под себя.

Обратите внимание, что скрипт полифила не может создать псевдоэлемент ::backdrop, поэтому стили для него вам нужно дублировать и для <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%);
}

        
        
          
        
      
Открыть демо в новой вкладке