<dialog>

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

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

Кратко

Секция статьи "Кратко"

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

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

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

Пример

Секция статьи "Пример"
        
          
          <dialog>  Привет, мир!</dialog>
          <dialog>
  Привет, мир!
</dialog>

        
        
          
        
      

Как пишется

Секция статьи "Как пишется"

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

Полноценно поддерживается только в последних версиях браузеров. Проверяйте поддержку на Can I Use.

Как открыть

Секция статьи "Как открыть"

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

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

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

        
        
          
        
      

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

  1. show() — добавляет атрибут open.
  2. showModal() — открывает в режиме «модального окна». Появляется подложка, в виде псевдоэлемента ::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>

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

Как закрыть

Секция статьи "Как закрыть"

Закрыть диалог можно как с помощью метода close() в JavaScript, так и в HTML с помощью формы с method="dialog".

        
          
          <dialog open="open" id="closeMe">  <h1>Закрой меня! 🙏</h1>  <p>Результат этих кнопок одинаковый</p>  <button type="button" onclick="window.closeMe.close();">    Закрыть с помощью JS  </button>  <form method="dialog">    <button>Закрыть с помощью формы</button>  </form></dialog>
          <dialog open="open" id="closeMe">
  <h1>Закрой меня! 🙏</h1>
  <p>Результат этих кнопок одинаковый</p>
  <button type="button" onclick="window.closeMe.close();">
    Закрыть с помощью JS
  </button>
  <form method="dialog">
    <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".
  • Модальные диалоги закрываются по нажатию на Esc.
  • У модального диалога при открытии появляется «ловушка фокуса»: для клавиатурной навигации доступны только интерактивные элементы только текущего диалога.
  • Браузер запоминает какой элемент был в фокусе до открытия окна и после закрытия окна снова переводит его в фокус.

Вся это логика реализована в самом браузере «из коробки». А значит мы отправляем пользователю меньше трафика.

Подсказки

Секция статьи "Подсказки"

💡 В Google Chrome при закрытии модального окна по Esc ставит предыдущий элемент не просто :focus, а в :focus-visible. Подразумевая что пользователь перешёл на клавиатурную навигацию.

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

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

💡 Модальные окна «ускользают» от контекста. Даже если вы разметили модальное окно в вёрстке перед <div> с z-index: 99999 модальное окно всё равно откроется поверх него. Или если вы наклонили родителя <dialog> с помощью 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

Секция статьи "Закрываем по клику на ::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%);
}

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