Как сделать модальное окно на HTML, CSS и JS: элемент dialog
Модальное окно собирается на элементе dialog: браузер сам затемняет страницу, удерживает фокус и закрывает окно по Esc. Показываем код, вариант без JavaScript и поддержку браузеров.
Модальное окно на HTML, CSS и JS собирается на элементе dialog. Метод showModal() открывает окно поверх страницы, затемняет её и блокирует, а клавиша Esc закрывает окно без нашего кода. Открыть и закрыть окно можно и без JavaScript, атрибутами commandfor и command у кнопки. Закрытие по клику мимо окна включает атрибут closedby, а в Safari для этого пока нужен запасной обработчик.
Коротко
- Элемент
dialogработает во всех основных браузерах с марта 2022 года: Chrome 37, Firefox 98, Safari 15.4. showModal()открывает окно как модальное: поверх страницы, с затемнением::backdropи закрытием по Esc. После закрытия браузер возвращает фокус на кнопку, которая открыла окно.- Кнопка с атрибутами
commandforиcommand="show-modal"открывает окно без JavaScript. Атрибуты понимают Chrome и Edge 135, Firefox 144 и Safari 26.2. closedby="any"закрывает окно по клику на затемнение в Chrome и Edge 134 и в Firefox 141. В стабильном Safari атрибут не работает.- Ответ пользователя приходит в свойство
returnValue. Перед каждым открытием его стоит очищать. - Страница под открытым окном продолжает прокручиваться. Прокрутку отключает одно правило CSS.
Как сделать модальное окно на dialog
Модальное, или всплывающее, окно собирается из элемента dialog с содержимым, кнопки на странице и вызова showModal() по клику. Пока окно закрыто, браузер его не показывает.
<button class="open" type="button">Открыть окно</button>
<dialog class="modal">
<h2>Модальное окно</h2>
<p>Страница под окном недоступна, пока оно открыто. Закрыть можно кнопкой или клавишей Esc.</p>
<button class="close" type="button">Закрыть</button>
</dialog>
const modal = document.querySelector('.modal');
document.querySelector('.open').addEventListener('click', () => {
modal.showModal();
});
document.querySelector('.close').addEventListener('click', () => {
modal.close();
});
После вызова showModal() браузер сам делает то, что в окне на div приходится писать руками:
- переносит окно в верхний слой (top layer), который рисуется поверх всей страницы при любых
z-indexу других блоков; - рисует под окном затемнение на весь экран, псевдоэлемент
::backdrop; - делает остальную страницу инертной: по ней нельзя кликнуть, и на неё нельзя перейти клавишей Tab;
- ставит фокус внутрь окна;
- закрывает окно по Esc и возвращает фокус на элемент, с которого окно открыли.
Мы проверили пример в Chrome 154. После клика фокус стоит на кнопке «Закрыть», после Esc он возвращается на кнопку «Открыть окно». Клик в точку, где под затемнением лежит кнопка «Открыть окно», до неё не доходит.
В живом примере окну уже заданы ширина, отступы и цвет затемнения. Эти стили разобраны в следующем разделе.
Под примером стоит ссылка на пен: он открывается в редакторе Penlab, и код в нём можно менять.
Открыть dialog можно тремя способами, и модальным окно получается только в одном:
showModal()открывает модальное окно: с затемнением, блокировкой страницы и закрытием по Esc.show()открывает окно без затемнения и без блокировки страницы. Клавиша Esc такое окно не закрывает.- Атрибут
open, поставленный в разметке или добавленный из скрипта, даёт то же немодальное окно, что иshow().
Руками мы open не ставим и не снимаем. По спецификации HTML после снятия open у модального окна событие close не придёт, и страница останется заблокированной.
Как оформить окно и затемнение
У dialog есть стили браузера: сплошная рамка, отступы в 1em, центрирование через margin: auto и почти прозрачное затемнение. Обычно мы убираем рамку, задаём ширину и делаем фон ::backdrop темнее.
.modal {
width: min(90vw, 360px);
padding: 24px;
border: 0;
border-radius: 8px;
}
.modal::backdrop {
background-color: rgb(0 0 0 / 0.5);
}
.modal h2 {
margin-top: 0;
}
В стилях браузера затемнение задано как rgba(0, 0, 0, 0.1), то есть чёрный цвет с непрозрачностью 10%. На светлой странице его почти не видно, поэтому фон ::backdrop задаём свой.
Стили браузера чаще всего дают о себе знать в таких случаях:
- Окно уехало в угол. По центру его держит
margin: autoиз стилей браузера. Общий сбросmargin: 0центрирование отключает. - Закрытое окно видно на странице. Его прячет правило браузера
dialog:not([open]) { display: none; }. Нашеdisplay: flexилиdisplay: gridна селекторе.modalэто правило перебивает. Раскладку пишем на.modal[open]или на блоке внутри окна. - Окно не растягивается на весь экран. У модального окна браузер ограничивает размеры:
max-widthиmax-heightравныcalc(100% - 6px - 2em). Для окна во весь экран оба свойства переопределяем, например значением100%.
Плавное появление окна и затемнения тоже пишется на CSS. Этот код мы разобрали в статье «Плавное появление блока в CSS: как анимировать display: none».
Как открыть и закрыть окно без JavaScript
Кнопку связывают с элементом dialog атрибуты commandfor и command. В первый записываем id окна, во второй - действие: show-modal открывает окно как модальное, close закрывает его.
<button type="button" commandfor="modal" command="show-modal">Открыть окно</button>
<dialog id="modal" class="modal" closedby="any">
<h2>Окно без JavaScript</h2>
<p>Кнопки связаны с окном атрибутами. Там, где работает closedby, окно закрывает и клик по затемнению.</p>
<button type="button" commandfor="modal" command="close">Закрыть</button>
</dialog>
Окно открывается так же, как после showModal(): с затемнением, заблокированной страницей и закрытием по Esc. Атрибуты commandfor и command работают в Chrome и Edge с версии 135, в Firefox с версии 144 и в Safari с версии 26.2.
В живом примере у окна те же стили, что и в предыдущем разделе, а панели JS в нём нет.
Кнопке внутри формы нужен type="button". Если кнопка с commandfor стоит внутри формы и type у неё не указан, команда не выполняется: в Chrome 154 такая кнопка не закрыла окно и не отправила форму.
В браузере без поддержки commandfor у кнопки нет действия: незнакомый атрибут браузер пропускает. В таком браузере мы пример не запускали, это следует из правил разбора HTML. Если на сайт заходят с таких браузеров, добавляем проверку и открываем окно из скрипта:
const modal = document.querySelector('#modal');
const openButton = document.querySelector('[command="show-modal"]');
if (!('commandForElement' in HTMLButtonElement.prototype)) {
openButton.addEventListener('click', () => {
modal.showModal();
});
}
Кнопку закрытия в этом случае надёжнее положить в форму с method="dialog": такая форма закрывает окно во всех браузерах, где есть dialog.
<form method="dialog">
<button>Закрыть</button>
</form>
Как закрыть окно по клику на фон
За клик мимо окна отвечает атрибут closedby у элемента dialog. Со значением any окно закрывается и по Esc, и по клику на затемнение. Отдельный обработчик не нужен.
У атрибута три значения:
any- сработают и Esc, и клик мимо окна.closerequest- только Esc и системные способы закрытия, например кнопка «Назад» на Android. Так ведёт себя окно, открытое черезshowModal(), когда атрибут не указан.none- закрыть окно могут только наши кнопки и скрипт.
Атрибут closedby работает в Chrome и Edge с версии 134 и в Firefox с версии 141. В Safari он появился в апреле 2026 года в Technology Preview 242, предварительной сборке браузера для разработчиков, и в стабильном Safari 27 его нет. Для Safari и для старых версий остальных браузеров пишем запасной обработчик.
У клика по затемнению event.target - сам элемент dialog. Тот же target даёт клик по внутренним отступам окна. Мы обнуляем padding у dialog и переносим отступы на блок внутри него, чтобы любой клик внутри окна попадал во вложенный элемент. В нашем примере этот блок - форма с кнопками окна.
<dialog class="modal">
<form method="dialog">
<p>Содержимое окна</p>
<button>Закрыть</button>
</form>
</dialog>
.modal {
padding: 0;
}
.modal form {
padding: 24px;
}
const modal = document.querySelector('.modal');
if (!('closedBy' in HTMLDialogElement.prototype)) {
let pressedOnBackdrop = false;
modal.addEventListener('pointerdown', (event) => {
pressedOnBackdrop = event.target === modal;
});
modal.addEventListener('click', (event) => {
if (pressedOnBackdrop && event.target === modal) {
modal.close();
}
});
}
Обработчик pointerdown нужен для одного случая. Пользователь выделяет текст в окне, ведёт мышь за его край и отпускает кнопку над затемнением. Событие click при этом приходит на dialog, и обработчик, который проверяет только event.target, закроет окно. Мы воспроизвели это в Chrome 154: вариант без pointerdown закрыл окно, вариант с ним оставил открытым.
Оба обработчика мы проверяли в Chrome 154 на окне без атрибута closedby и без условия if: в этом браузере свойство closedBy есть, и код под условием не выполняется. В Safari вживую их не запускали.
Как узнать, что ответил пользователь
Кнопки окна кладём в форму с method="dialog". Такая форма никуда не отправляется: она закрывает окно и записывает значение value нажатой кнопки в свойство returnValue. Читаем его в обработчике события close.
<dialog class="modal">
<form method="dialog">
<h2>Удалить черновик?</h2>
<button value="cancel">Отмена</button>
<button value="delete">Удалить</button>
</form>
</dialog>
const modal = document.querySelector('.modal');
modal.addEventListener('close', () => {
console.log(modal.returnValue); // 'cancel' или 'delete'
});
Событие close приходит при любом закрытии: кнопкой формы, клавишей Esc, кликом по затемнению, вызовом close(). Значение можно передать и из скрипта, modal.close('delete'), и через value кнопки с command="close".
Тут есть нюанс. По спецификации Esc и клик мимо окна returnValue не меняют, в свойстве остаётся значение с прошлого закрытия. Мы проверили такую последовательность в Chrome 154: нажали «Удалить», открыли окно снова и кликнули по затемнению. В returnValue осталось delete, и страница сообщила об удалении, хотя пользователь отказался.
С клавишей Esc браузеры расходятся. В нашем тесте Chrome 154 после Esc записал в свойство пустую строку, а в вопросе на Stack Overflow описан Firefox 143, который оставил прошлое значение. Чтобы результат не зависел от браузера и способа закрытия, перед каждым открытием свойство очищаем:
modal.returnValue = '';
modal.showModal();
Куда поставить фокус и как дать окну имя
Без подсказок браузер ставит фокус на первый элемент окна, до которого можно дойти клавишей Tab. Нужный элемент мы отмечаем атрибутом autofocus. В окне подтверждения это кнопка отмены: случайное нажатие Enter ничего не удалит.
<dialog class="modal" aria-labelledby="modal-title">
<form method="dialog">
<h2 id="modal-title">Удалить черновик?</h2>
<button value="cancel" autofocus>Отмена</button>
<button value="delete">Удалить</button>
</form>
</dialog>
Атрибут aria-labelledby связывает окно с его заголовком, и программа чтения с экрана при открытии называет окно этим текстом. Роль dialog, по которой программа чтения с экрана узнаёт окно, у элемента уже есть. Окно, открытое через showModal(), браузер сам помечает как модальное. Писать role="dialog" и aria-modal="true" на dialog не нужно. Атрибут tabindex на нём спецификация запрещает.
Видимую кнопку закрытия мы оставляем в каждом окне: клавиша Esc есть не на всех устройствах. С программами чтения с экрана мы примеры не проверяли, этот раздел опирается на спецификацию HTML, MDN и руководство W3C по модальным окнам.
Как запретить прокрутку страницы под окном
Метод showModal() блокирует клики и фокус, но прокрутку страницы оставляет. Её отключает одно правило с селектором :has(): пока на странице есть открытое модальное окно, у html стоит overflow: hidden.
html:has(dialog:modal) {
overflow: hidden;
}
В Chrome 154 на странице высотой 3000 пикселей без этого правила колесо мыши над затемнением прокрутило страницу на 400 пикселей. С правилом страница осталась на месте.
Селектор :has() работает в Chrome и Edge с версии 105, в Firefox с версии 121 и в Safari с версии 15.4. Псевдокласс :modal - в Chrome и Edge 105, Firefox 103 и Safari 15.6.
Вместе с прокруткой исчезает полоса прокрутки, и там, где она занимает место, страница сдвигается вбок. Место под полосу резервирует scrollbar-gutter: stable на элементе html. Свойство есть в Chrome и Edge 94, Firefox 97 и Safari 18.2.
В каких браузерах работают dialog, commandfor и closedby
Базовое окно на dialog работает во всех актуальных браузерах. От версии зависят открытие без JavaScript и закрытие по клику на фон.
Данные собраны 12 октября 2026 года по таблицам совместимости MDN. Стабильные версии на этот день: Chrome и Edge 155, Firefox 157, Safari 27.
dialog,showModal(),close(),returnValue,::backdropи форма сmethod="dialog": Chrome 37, Edge 79, Firefox 98, Safari 15.4.- Атрибуты
commandforиcommand: Chrome и Edge 135, Firefox 144, Safari 26.2. - Атрибут
closedby: Chrome и Edge 134, Firefox 141. В Safari только в предварительной сборке Technology Preview 242. - Селектор
:has(): Chrome и Edge 105, Firefox 121, Safari 15.4. - Псевдокласс
:modal: Chrome и Edge 105, Firefox 103, Safari 15.6.
Примеры из статьи мы запускали в Chrome 154. В Firefox и Safari вживую их не проверяли, поведение в этих браузерах описано по документации.
Готовое окно подтверждения: код целиком
Окно подтверждения собрано из частей, разобранных выше: форма с method="dialog", фокус на кнопке отмены, закрытие по клику на фон с запасным кодом, запрет прокрутки и вывод ответа на страницу. Открывает окно скрипт, потому что перед открытием он очищает returnValue.
<button class="open" type="button">Удалить черновик</button>
<p class="result">Ответа пока нет.</p>
<dialog class="modal" closedby="any" aria-labelledby="modal-title">
<form method="dialog">
<h2 id="modal-title">Удалить черновик?</h2>
<p>Вернуть его после удаления не получится.</p>
<div class="actions">
<button value="cancel" autofocus>Отмена</button>
<button value="delete">Удалить</button>
</div>
</form>
</dialog>
.modal {
width: min(90vw, 360px);
padding: 0;
border: 0;
border-radius: 8px;
}
.modal form {
padding: 24px;
}
.modal::backdrop {
background-color: rgb(0 0 0 / 0.5);
}
.modal h2 {
margin-top: 0;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
html:has(dialog:modal) {
overflow: hidden;
}
const modal = document.querySelector('.modal');
const result = document.querySelector('.result');
document.querySelector('.open').addEventListener('click', () => {
modal.returnValue = '';
modal.showModal();
});
modal.addEventListener('close', () => {
if (modal.returnValue === 'delete') {
result.textContent = 'Черновик удалён.';
} else {
result.textContent = 'Удаление отменено.';
}
});
// Запасной вариант для браузеров без атрибута closedby
if (!('closedBy' in HTMLDialogElement.prototype)) {
let pressedOnBackdrop = false;
modal.addEventListener('pointerdown', (event) => {
pressedOnBackdrop = event.target === modal;
});
modal.addEventListener('click', (event) => {
if (pressedOnBackdrop && event.target === modal) {
modal.close();
}
});
}
На этом пене можно проверить, зачем нужна очистка returnValue. Открываем его в редакторе Penlab по ссылке под примером и повторяем шаги:
- Убираем из кода строку
modal.returnValue = '';. - Нажимаем «Удалить черновик», затем «Удалить».
- Открываем окно снова.
- Кликаем по затемнению: на странице остаётся «Черновик удалён.», хотя в этот раз мы отказались. Если вернуть строку из шага 1, тот же клик меняет надпись на «Удаление отменено.».
Своё окно с нуля удобно собрать в новом пене. Готовый пен можно отправить ссылкой или встроить на свою страницу. Об этом есть заметка «Поделиться пеном и встроить его на свой сайт».
FAQ
Нужно ли ещё делать модальное окно на div?
В большинстве случаев нет. Элемент dialog работает во всех основных браузерах с марта 2022 года, а окну на div приходится вручную добавлять то, что dialog даёт сразу: блокировку страницы, управление фокусом и закрытие по Esc.
Чем dialog отличается от popover?
Модальное окно dialog блокирует страницу под собой, блок с атрибутом popover оставляет её доступной. Для меню, подсказок и выпадающих списков подходит popover. Делать такие элементы на dialog спецификация HTML называет ошибкой.
Почему окно не закрывается после close()?
Частая причина - правило display: flex или display: grid на самом dialog. Оно перебивает правило браузера, которое прячет окно без атрибута open. Окно закрыто, но остаётся на экране. Раскладку переносим на селектор dialog[open] или на блок внутри окна.
Как плавно открыть и закрыть модальное окно?
Плавное открытие и закрытие делается на CSS: правило @starting-style задаёт начальный стиль, а display и overlay в списке переходов удерживают окно на экране, пока идёт скрытие. Код для dialog и ::backdrop есть в статье «Плавное появление блока в CSS: как анимировать display: none».
Можно ли запретить закрытие по Esc?
Да, атрибутом closedby="none" в браузерах, которые его поддерживают. В Chrome 154 окно с этим значением не закрылось ни по Esc, ни по клику на затемнение. Второй способ - отменить событие cancel через preventDefault(), но по спецификации отмена срабатывает не всегда: если между нажатиями Esc пользователь ничего не делал на странице, браузер вправе закрыть окно. Мы бы оставляли Esc рабочим везде, где закрытие окна ничего не ломает.
Можно ли положить в окно форму с полями?
Да. Форма с method="dialog" данные на сервер не отправляет: она закрывает окно, а значения полей остаются в форме, и их можно прочитать в обработчике события close. В Chrome 154 поле сохранило введённый текст после закрытия окна такой формой.
Почему подсказка или уведомление оказывается под окном?
Модальное окно рисуется в верхнем слое, и z-index остальных элементов страницы на этот слой не влияет. Всё за пределами окна к тому же инертно: не отвечает на клики и клавишу Tab. Уведомление, которое должно быть видно поверх окна, размещаем внутри самого dialog.
Источники
- HTML Standard: элемент dialog
- MDN: элемент dialog
- Chrome for Developers: Introducing command and commandfor
- Stack Overflow: как закрыть dialog кликом мимо окна
- WebKit: Release Notes for Safari Technology Preview 242
- HTML Standard: атрибуты command и commandfor
- MDN: HTMLDialogElement.showModal()
- W3C APG: Dialog (Modal) Pattern
- Chrome for Developers: New in Chrome 134
- Stack Overflow: кнопка закрытия dialog не работает, ответ про display
- HTML Standard: стили браузера для dialog
- MDN: HTMLDialogElement.returnValue
- WebKit: Introducing the Dialog Element
- W3C: ARIA in HTML
- whatwg/html, issue 7732: прокрутка страницы под модальным окном
- MDN: таблицы совместимости browser-compat-data
- Hidde de Vries: Dialogs and popovers seem similar. How are they different?
- Stack Overflow: returnValue сохраняет прошлое значение