Создание элемента popup: нативный HTML-тег <dialog>

📂 Категория: HTML
🏷️ Теги:

Много лет для создания модальных окон (Popup) использовался стандартный элемент <div> вместе со скриптами управления видимостью, ловушками фокуса (focus trap) и ручной настройкой слоев. Но уже довольно давно в стандартах появился нативный элемент <dialog>, который решает все эти задачи из коробки.

Заметка
На июль 2026 поддержка браузерами составляет 96%.

У него есть ряд преимуществ по сравнению со стандартным методом:

  • Поддержка закрытия окна по клавише 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>

Заголовок модального окна

Введите имя

Должно содержать не менее 6 символов

Показать 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, делая код чище, а производительность — выше.

Похожие статьи