Много лет для создания модальных окон (Popup) использовался стандартный элемент <div> вместе со скриптами управления видимостью, ловушками фокуса (focus trap) и ручной настройкой слоев. Но уже довольно давно в стандартах появился нативный элемент <dialog>, который решает все эти задачи из коробки.
У него есть ряд преимуществ по сравнению со стандартным методом:
- Поддержка закрытия окна по клавише
Esc - Окно попадает в
top layerбраузера, поэтому всегда выше элементов с любымz-index - Встроенный
backdrop - Нативная поддержка скринридерами (доступность / a11y)
- Автоматический фокус на первом элементе внутри
<dialog>. Плюс фокус не выходит за пределы модального окна.
Скрипты довольно простые. Открытие происходит через modal.showModal() для полноэкранного modal. Или modal.show() для неблокирующего страницу окна вроде надоедалки об использовании cookie или плавающих панелей. Закрытие выполняется методом modal.close().
Пример
В данном примере прописываем небольшие стили, чтобы modal не прилипал к экрану, а страница не проскролливалась, когда он открыт. Также есть плавные анимации открытия и закрытия, включая backdrop.
<button data-modal-open-btn="modal-1" type="button">Открыть Popup</button>
Показать HTML код
<dialog class="popup" id="modal-1">
<button class="close-button" type="button" aria-label="Закрыть" data-modal-close-btn>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
<p class="text-lg font-bold">Заголовок модального окна</p>
<fieldset class="fieldset mt-1">
<legend class="fieldset-legend">Введите имя</legend>
<input type="text" class="input" placeholder="Type here" />
<p class="label">Должно содержать не менее 6 символов</p>
</fieldset>
</dialog>Показать CSS код
html {
scrollbar-gutter: stable;
}
body:has(dialog:modal) {
overflow: hidden;
}
.popup {
--_duration: 500ms;
background: white;
position: fixed;
inset: 1rem;
margin: auto;
height: fit-content;
max-width: 30rem;
scale: 0.5;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: none;
border-radius: 1rem;
border: none;
padding: 1.5rem;
color: black;
opacity: 0;
transition-behavior: allow-discrete;
transition-duration: var(--_duration);
@media (width >= 48rem /* 768px */) {
inset: 1.5rem;
}
&::backdrop {
background: color-mix(in oklab, black 60%, transparent);
opacity: 0;
transition-duration: var(--_duration);
}
& .close-button {
--_icon-size: 1.5rem;
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: var(--_icon-size);
height: var(--_icon-size);
transition-duration: 300ms;
& svg {
width: var(--_icon-size);
height: var(--_icon-size);
}
&:hover {
color: color-mix(in oklab, black 60%, transparent);
}
}
}
.popup[open] {
scale: 1;
opacity: 1;
&::backdrop {
opacity: 1;
}
}
@starting-style {
.popup[open] {
scale: 0.5;
opacity: 0;
&::backdrop {
opacity: 0;
}
}
}Показать JavaScript код
const modalOpenButtons = document.querySelectorAll('[data-modal-open-btn]')
modalOpenButtons.forEach(button =>
button.addEventListener('click', () => {
const id = button.dataset.modalOpenBtn
const modal = document.querySelector(`#${id}`)
if (modal instanceof HTMLDialogElement) modal.showModal()
}),
)
/**
* @param {MouseEvent} event
*/
function closeOnBackdropClick(event) {
const modal = event.currentTarget
if (!(modal instanceof HTMLDialogElement)) return
const rect = modal.getBoundingClientRect()
const isClickOutside =
event.clientX < rect.left ||
event.clientX > rect.right ||
event.clientY < rect.top ||
event.clientY > rect.bottom
if (isClickOutside) modal.close()
}
const modals = document.querySelectorAll('dialog')
modals.forEach(modal => modal.addEventListener('click', closeOnBackdropClick))
const closeModalButtons = document.querySelectorAll('[data-modal-close-btn]')
closeModalButtons.forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('dialog')
modal?.close()
})
})В небольшом скрипте прописано:
- Открытие
<dialog>с id, указанным вdata-modal-open-btnу кнопки - Закрытие
<dialog>при клике по егоbackdrop - Закрытие
<dialog>при клике на кнопку внутри него с атрибутомdata-modal-close-btn
React
Для React тоже нет препятствий в использовании, хотя концептуально <dialog> управляется императивными методами (showModal() / close()), в то время как React живет в декларативном мире состояний. Чтобы их поддружить, достаточно привязать useRef и синхронизировать открытие/закрытие через useEffect по изменению флага состояния:
function MyModal({ isOpen, onClose }) {
const dialogRef = useRef<HTMLDialogElement>(null)
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (isOpen && !dialog.open) dialog.showModal()
else if (!isOpen && dialog.open) dialog.close()
}, [isOpen])
return (
<dialog ref={dialogRef} onClose={onClose}>
{/* контент */}
</dialog>
)
}
Итоги
Можно сказать, что сейчас нет причин не использовать именно такой вариант, даже при работе с фреймворками.
Использование нативного <dialog> полностью избавляет от необходимости подключать тяжелые сторонние библиотеки для модальных окон или писать громоздкий JS для управления доступностью и фокусом. Браузер берет на себя всю рутину по изоляции контента в top layer, поддержке Esc и обработке backdrop. Благодаря простой интеграции через useRef этот подход отлично вписывается в любые современные фреймворки вроде React, делая код чище, а производительность — выше.