Написать пост

Как использовать html-элемент dialog

Аватарка пользователя Александр Григоренко

Рассказали про особенности нативного HTML-элемента dialog, который позволяет создавать диалоговые окна в браузерах без JS.

Обложка поста Как использовать html-элемент dialog

Всем привет! Меня зовут Александр Григоренко, я фронтенд-разработчик. В основном, занимаюсь разработкой приложений на React, но также постоянно экспериментирую с различными технологиями.

В своей работе я часто создаю собственные или использую уже готовые UI-компоненты. Проблема с такими компонентами заключается в том, что они часто ограничены определённым фреймворком, и их реализация требует написания сложной нестандартизированной логики. В течение долгого времени для базовых UI-компонентов, таких как диалоговые окна, использовались самописные решения, а в тяжёлых случаях и встроенные в JavaScript методы alert(), prompt() и confirm().

Отличная новость в том, что такой компонент можно реализовать с использованием нативного HTML-элемента <dialog>, который встроен в стандарт HTML5 и работает одинаково во всех современных браузерах.

В статусе рабочего черновика W3C тег <dialog> появился в мае 2013-го года вместе с такими интерактивными элементами, как <details> и <summary>, предназначенными для решения классических интерфейсных задач. С 2014-го года <dialog> был доступен только в браузерах Google Chrome и Opera, а в Firefox и Safari полноценная поддержка появилась лишь в марте 2022-го года. По этой причине <dialog> довольно редко использовался в реальных проектах. Однако с учётом почти двухлетней поддержки основными браузерами, стандарт стал достаточно устойчивым, чтобы с уверенностью заменить самописные <div class="modal" tabindex="-1" role="dialog" aria-modal="true"> на нативную реализацию.

Давайте познакомимся с возможностями <dialog> поближе.

Основные особенности использования

HTML-тег <dialog> создаёт скрытое по умолчанию диалоговое окно на странице, которое может функционировать в двух режимах: в качестве всплывающего поп-апа или в роли модального окна.

Всплывающие поп-апы обычно используются для показа ненавязчивых уведомлений, таких как сообщения об использовании на сайте файлов cookie, автоматически исчезающих toast-сообщений, тултипов и даже элементов, имитирующих контекстное меню, вызываемое нажатием правой клавиши мыши.

Модальные окна применяются, когда необходимо сосредоточить внимание пользователя на конкретной задаче. Это могут быть уведомления и предупреждения, требующие от пользователя подтверждения действий на странице, сложные интерактивные формы, и, например, лайтбоксы для просмотра изображений или видеороликов.

Всплывающий поп-ап не мешает взаимодействию со страницей, в отличие от модального окна, которое открывается поверх всего документа, затемняет фон вокруг себя и блокирует любые действия с остальным контентом. Эта логика работает без необходимости в дополнительных стилях и скриптах; единственное отличие заключается в том, какой метод вызывается для открытия диалога.

Методы для открытия диалогового окна

Всплывающий поп-ап:

			<dialog id="pop-up">Привет, я поп-ап!</dialog>
		
			const popUpElement = document.getElementById("pop-up");

popUpElement.show();
		

Модальное окно:

			<dialog id="modal">Привет, а я — модалка!</dialog>
		
			сonst modalElement = document.getElementById("modal");

modalElement.showModal();
		

В обоих случаях при открытии окна тегу <dialog> проставляется булевый атрибут open в значении true. Значение атрибута можно установить в true напрямую, однако в этом случае диалоговое окно откроется как поп-ап — работать с ним как с модалкой просто не получится. Поэтому для рендеринга модальных окон необходимо использовать только соответствующий метод. Для создания изначально открытого поп-апа можно обойтись и без JS:

			<dialog open>Привет, я поп-ап!</dialog>
		

Попробовать в деле:

Способы закрытия диалогового окна

Закрываются диалоговые окна одинаково, независимо от того, каким образом они были открыты. Вот несколько способов закрыть всплывающее или модальное окно:

Через вызов метода .close():

			сonst dialogElement = document.getElementById("dialog");

dialogElement.close();
		

Через инициацию события submit в контексте формы с атрибутом method="dialog":

			<dialog>
  <h2>Закрой меня!</h2>

  <form method="dialog">
     <button>Закрыть</button>
  </form>
</dialog>
		

Нажатием клавиши Esc. Закрытие с помощью клавиши Esc работает только для модальных окон. При закрытии таким способом сначала запускается событие cancel, и только потом close — так, например, удобно предупреждать пользователя о том, что изменённые данные в форме внутри модалки не сохранятся.

Попробовать в деле:

Возвращаемое значение при закрытии

При закрытии диалогового окна через форму с атрибутом method="dialog" можно получить и обработать значение, указывающее на кнопку, которая была нажата перед закрытием. Это удобно, если после нажатия разных закрывающих кнопок требуется выполнить разные действия на странице. Для этого можно обратиться к свойству элемента диалогового окна returnValue, которое будет содержать значение атрибута value той кнопки, на которую нажал пользователь, чтобы закрыть окно.

Попробовать в деле.

Подробнее про механику работы

Рассмотрим более подробно механику работы диалогового окна и детали браузерной реализации.

Механика работы всплывающего поп-апа

Если элемент <dialog> был открыт как всплывающий поп-ап через метод .show() или напрямую через указание атрибута open, движок браузера автоматически разместит поп-ап в виде абсолютно спозиционированного блочного элемента в том месте, где он был указан в DOM. Для этого элемента будут применены базовые CSS-стили, включая отступы и границы, а первый фокусируемый элемент внутри окна получит фокус автоматически через глобальный атрибут autofocus. При этом сохранится возможность взаимодействия с остальной частью страницы.

Механика работы модального окна

Модальное окно устроено и работает несколько сложнее, чем поп-ап.

Перекрытие документа

При открытии модального окна с использованием метода .showModal() элемент <dialog> рендерится в специальном слое HTML-документа. Этот слой охватывает всю ширину и высоту видимой области страницы, располагаясь поверх всего документа. Такой слой называется верхним слоем документа (top layer), и является внутренней концепцией браузера — напрямую управлять им невозможно. В определённых браузерах, например, в Google Chrome, каждое модальное окно рендерится в отдельном DOM-узле верхнего слоя, которые можно увидеть в инспекторе элементов:

Как использовать html-элемент dialog 1

Понятие слоёв относится к концепции контекста наложения (stacking context), описывающей, как элементы располагаются относительно друг друга вдоль оси Z по отношению к пользователю, находящемуся перед экраном. Например, при задании значения CSS-свойства z-index для элемента, мы создаём замкнутый на этом элементе контекст наложения. Так позиция элемента будет рассчитываться относительно позиций его соседей, а все значения z-index дочерних элементов будут учитываться только в рамках контекста наложения родителя. Такую иерархию контекстов наложения можно представить в виде слоистой структуры, а открытое модальное окно всегда будет находиться наверху этой иерархии, так как оно рендерится в верхнем слое, и для него не нужно устанавливать CSS-правило z-index.

Подробнее про stacking context можно почитать тут. Подробнее про то, какие элементы рендерятся в top layerтут.

Блокировка документа

Когда элемент модального окна рендерится в верхнем слое, под ним создаётся псевдо-элемент подложки ::backdrop, которому устанавливаются размеры текущей видимой области документа. Эта подложка блокирует действия на остальной странице, даже если для неё установлено CSS-свойство pointer-events: none.

Дополнительная блокировка пользовательских действий обеспечивается путём автоматической установки глобального атрибута inert для всех элементов, за исключением модального окна. Атрибут inert предотвращает срабатывание событий клика и фокусировки в пределах элементов, для которых он установлен, а также прячет их от экранных дикторов (скринридеров) и других вспомогательных технологий, обеспечивающих доступность (accessibility).

Подробнее про атрибут inert.

Поведение фокуса

Первый фокусируемый элемент внутри модалки автоматически попадёт в фокус в момент её открытия. Для изменения элемента, который будет иметь изначальный фокус, можно воспользоваться атрибутами autofocus или tabindex. Установка tabindex для элемента диалогового окна невозможна, поскольку он, в любом случае, является единственным элементом страницы, для которого не применяется логика атрибута inert.

При закрытии диалогового окна фокус возвращается на тот элемент, который вызвал его открытие.

Решение проблем взаимодействия с модальными окнами

К сожалению, нативная реализация элемента <dialog> не охватывает все аспекты взаимодействия с модальными окнами. Далее, я предлагаю рассмотреть решения основных UX-проблем, которые могут возникнуть при использовании модальных окон.

Блокировка скролла

Хотя в нативной реализации модального окна и создаётся псевдоэлемент ::backdrop, который находится поверх страницы и блокирует взаимодействие с контентом — скролл страницы всё ещё доступен. Это может отвлекать пользователя, поэтому при открытии модального окна рекомендуется обрезать содержимое body:

			body {
  overflow: hidden;
}
		

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

			// При открытии модалки
document.body.classList.add("scroll-lock");

// При закрытии модалки
document.body.classList.remove("scroll-lock");
		

Также можно воспользоваться селектором :has, если статус поддержки этого селектора соответствует требованиям проекта:

			body:has(dialog[open]) {
  overflow: hidden;
}
		

Попробовать в деле.

Закрытие диалога по клику на свободной области

Это стандартный UX-сценарий для модального окна и он может быть реализован несколькими способами. Предлагаю ознакомиться с двумя способами решения этой проблемы:

Способ, основанный на особенностях работы псевдоэлемента подложки ::backdrop

Клик по псевдоэлементу подложки рассматривается как клик по самому элементу диалога. Следовательно, если весь контент модального окна обернуть в дополнительный <div> и затем перекрыть им сам элемент диалога, можно будет определить, куда был направлен клик — на подложку или на содержимое модального окна.

Не забудем сбросить стандартные браузерные стили отступов и границ у элемента <dialog>, чтобы предотвратить закрытие модального окна при случайном клике по ним:

			dialog {
  padding: 0;
  border: none;
}
		

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

Осталось написать функцию, которая будет закрывать модальное окно только при клике на подложку, а не на внутренний элемент обёртки:

			const handleModalClick = ({ currentTarget, target }) => {
  const isClickedOnBackdrop = target === currentTarget;

  if (isClickedOnBackdrop) {
    currentTarget.close();
  }
}

modalElement.addEventListener("click", handleModalClick);
		

Попробовать в деле.

Способ, основанный на определении размеров диалогового окна

В отличие от первого способа, который требовал обёртывания внутреннего содержимого модального окна в дополнительный элемент, этот способ не требует использования дополнительной обёртки. Всё, что необходимо, — это проверить, выходят ли координаты курсора за пределы области элемента окна при клике:

			const handleModalClick = (event) => {
  const modalRect = modalElement.getBoundingClientRect();

  if (
    event.clientX < modalRect.left ||
    event.clientX > modalRect.right ||
    event.clientY < modalRect.top ||
    event.clientY > modalRect.bottom
  ) {
    modalElement.close();
  }
};

modalElement.addEventListener("click", handleModalClick);
		

Попробовать в деле.

Стилизация диалогового окна

В отличие от многих нативных HTML-элементов, элемент <dialog> предоставляет значительную гибкость в плане стилизации. Вот несколько готовых рецептов для стилизации диалоговых окон:

Доступность

Хотя долгое время элемент <dialog> имел некоторые проблемы с соответствием стандартам доступности (accessibility), на данный момент основные вспомогательные технологии, такие как экранные дикторы (VoiceOver, TalkBack, NVDA), хорошо работают с диалоговыми окнами.

При открытии элемента <dialog>, фокус экранного диктора переводится на диалоговое окно, а в случае с модалкой — остаётся в её пределах до тех пор, пока она открыта.

Нативный элемент <dialog> по умолчанию распознаётся вспомогательными технологиями как элемент с ARIA-атрибутом role="dialog". Элемент <dialog>, открытый как модальное окно, будет восприниматься как элемент с ARIA-атрибутом aria-modal="true".

Вот несколько рекомендаций, как улучшить доступность элемента <dialog>:

aria-labelledby

Всегда используйте заголовок внутри диалоговых окон и указывайте атрибут aria-labelledby для элемента <dialog>, со значением идентификатора заголовка:

			<dialog aria-labelledby="dialog-header">
  <h1 id="dialog-header">Dialog Header</h1>
</dialog>
		

В таком случае экранные дикторы будут зачитывать содержимое этого заголовка при открытии диалогового окна.

aria-describedby

Используйте атрибут aria-describedby для связи с содержимым диалогового окна. Некоторые скринридеры не смогут прочитать содержимое элемента <dialog> без этого атрибута. Заголовки и любые интерактивные элементы для управления состоянием диалогового окна должны быть вынесены отдельно за пределы элемента с содержимым:

			<dialog aria-labelledby="dialog-header" aria-describedby="dialog-content">
  <h1 id="dialog-header">Dialog Header</h1>

  <div id="dialog-content">
    Dialog Content
  </div>

  <button id="close-btn">Close dialog</button>
</dialog>
		

aria-label

Всегда добавляйте кнопку для закрытия диалоговых окон, особенно внутри модалок. Для лучшей доступности необходимо использовать именно элемент <button>. Для кнопок, которые не содержат очевидный для пользователя текст, необходимо указать этот текст в ARIA-атрибуте aria-label:

			<dialog aria-labelledby="dialog-header">
  <button id="close-btn" aria-label="Close dialog">x</button>

  <h1 id="dialog-header">Dialog Header</h1>
</dialog>
		

Браузерная поддержка

Нативный элемент диалогового окна представляет собой удобный и мощный инструмент для решения стандартных интерфейсных задач. К сожалению, его поддержка в основных браузерах была добавлена сравнительно недавно, и в более экзотических или устаревших браузерах поддержки всё ещё может не быть. При отсутствии поддержки нативного элемента <dialog>, можно воспользоваться полифилом, разработанным командой Google Chrome.

Скрипты и стили полифила можно подключить локально, использовать CDN или установить его как npm-зависимость: npm install dialog-polyfill.

Если полифил подключён не через импорт npm-пакета, не забудьте отдельно подключить стили.

Если требуется стилизовать псевдоэлемент подложки модального окна ::backdrop, убедитесь, что вы также применяете стили к соответствующему элементу с классом .backdrop для обеспечения совместимости с более старыми браузерами:

			dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}

dialog + .backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}
		

Подключать полифил рекомендуется через динамический импорт и только для тех клиентов, которые не поддерживают элемент <dialog>:

			const isBrowserNotSupportDialog = window.HTMLDialogElement === undefined;

if (isBrowserNotSupportDialog) {
  const dialogElement = document.getElementById("modal");

  const { default: polyfill } = await import("dialog-polyfill");

  polyfill.registerDialog(dialogElement);
}
		

Заключение

Нативный HTML-элемент <dialog> — это относительно простой и очень мощный инструмент для реализации модальных окон и поп-апов. Он отлично поддерживается современными браузерами и может успешно использоваться как в проектах на чистом JS, так и в контексте любого фронтенд-фреймворка.

В данной статье мы охватили следующие темы:

  • Проблемы, которые призван решить элемент <dialog>;
  • Взаимодействие с API элемента <dialog>;
  • Механика работы с диалоговыми окнами на уровне браузера;
  • Возможные проблемы при работе с модальными окнами и их решения;
  • Улучшение доступности элемента <dialog> для вспомогательных устройств, таких как скринридеры;
  • Расширение браузерной поддержки элемента <dialog>.

Напоследок приглашаю рассмотреть реализацию компонента модального окна на чистом JS, в которой учтены основные аспекты, описанные в статье.

Это всё, что я хотел бы рассказать про особенности работы с HTML-элементом <dialog>. Надеюсь, что данная статья вдохновит вас на эксперименты, жду ваших вопросов в комментариях!

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

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий3К показов