Создать модальное окно html
Тег для создания всплывающего окна без боли и страданий.
Время чтения: меньше 5 мин
Кратко
Скопировать ссылку «Кратко» Скопировано
Тег создаёт всплывающее окно или диалог. По умолчанию не показывается на странице.
Может открываться в двух режимах:
- всплывающее окно — не блокирует взаимодействие со страницей;
- модальное окно — откроется поверх страницы, имеет фоновое затемнение, остальной контент не доступен для взаимодействия.
Как пишется
Скопировать ссылку «Как пишется» Скопировано
Парный тег < / dialog>, внутри которого находится содержимое всплывающего окна. У нельзя использовать атрибут tabindex .
Привет, мир!
dialog> Привет, мир! dialog>
Как открыть
Скопировать ссылку «Как открыть» Скопировано
Я виден. Привет! 👋 Я скрыт от пользователя 🥷
dialog open> Я виден. Привет! 👋 dialog> dialog> Я скрыт от пользователя 🥷 dialog>
Также окно можно открыть с помощью JavaScript-методов:
- show ( ) — добавляет атрибут open .
- show Modal ( ) — открывает в режиме «модального окна». Появляется подложка в виде псевдоэлемента : : backdrop , который можно стилизовать.
button type="button" onclick="window.myDialog.show();">Просто открытьbutton> button type="button" onclick="window.myDialog.showModal();">Открыть как модалкуbutton> dialog id="myDialog">🖖 Живи долго и процветай!dialog>
Как закрыть
Скопировать ссылку «Как закрыть» Скопировано
- Из JavaScript с помощью метода close ( ) .
- Из HTML по событию submit (например по нажатию кнопки ), если в есть тег с атрибутом method = «dialog» .
Закрой меня! 🙏
Результат этих кнопок одинаковый
Закрыть с помощью JSdialog 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 . return Value .
Присвоим двум кнопкам разные значения:
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 ( ) , если красота была не важна.
Тег появился как альтернатива. Хорошее диалоговое окно — это не просто логика «Показать» и «Скрыть». В реализовано то, о чём часто забывают:
- Для инструментов доступности воспринимается как аналог role = «dialog» . А если оно открыто в режиме модального окна, то и как аналог aria — modal = «true» .
- Модальные диалоги закрываются по нажатию на Esc .
- У модального диалога при открытии появляется «ловушка фокуса»: для клавиатурной навигации доступны только интерактивные элементы только текущего диалога.
- Браузер запоминает какой элемент был в фокусе до открытия окна и после закрытия окна снова переводит его в фокус.
Вся это логика реализована в самом браузере «из коробки». А значит пользователю не отправляется лишний трафик.
Подсказки
Скопировать ссылку «Подсказки» Скопировано
💡 Google Chrome при закрытии модального окна клавишей Esc ставит предыдущий элемент не просто в :focus , а в :focus — visible . Подразумевая, что пользователь перешёл на клавиатурную навигацию.
💡 По нажатию Esc сначала запускается событие cancel , а затем close . Это может быть полезно, если мы хотим отгородить пользователя от случайного нажатия клавиши, сначала предупредив, что изменённые данные не сохранятся, и только при повторном нажатии закрывать окно.
💡 Контент по умолчанию скрыт с помощью display : none . Можно переписать это поведение в стилях и анимировать открытие и закрытие. Намного легче, чем аналогичная задача в например.
💡 Модальные окна «ускользают» от контекста: даже если в HTML-разметке после модального окна указан тег с z — index : 99999 , то модальное окно всё равно отобразится поверх этого . Или если родитель наклонён с помощью skew ( ) , то дочернее модальное окно всё равно откроется без наклона.
На практике
Скопировать ссылку «На практике» Скопировано
Артур Бэйлис Ли советует
Скопировать ссылку «Артур Бэйлис Ли советует» Скопировано
Блокируем скролл
Скопировать ссылку «Блокируем скролл» Скопировано
Несмотря на то, что модального окно перекрывает весь остальной контент на странице с помощью псевдоэлемента : : backdrop , вся остальная страница всё равно доступна для прокрутки. Это может смущать пользователя, если на заднем плане будет что-то мельтешить.
Так же с помощью 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-сценарий, что модальное окно закрывается по клику на подложку (оверлей). Поскольку для подложкой является псевдоэлемент : : backdrop , то просто навесить на него обработчик клика не выйдет.
Однако клик по : : backdrop считается и кликом по самому элементу . Значит можно обернуть весь контент модального окна в обёртку и отлавливать когда клик проходит по самому диалогу, а когда по контенту в нём.
Контент диалога
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 будет совпадать с 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 начали поддерживать только в марте 2022 года. Для продакшена большинства проектов, по крайней мере ближайшие несколько лет, нужно поддерживать и более старые версии браузеров. Что делать? Отказываться от такого удобного элемента?
К счастью, команда Google Chrome давно разработала полифил, который имитирует работу в старых браузерах. Всё что нужно это подключить скрипт и дополнительные стили.
Но стойте! Неужели ≈ 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 , поэтому стили для него вам нужно дублировать и для с классом .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%); >