---
title: "Как сделать модальное окно на HTML, CSS и JS: элемент dialog"
description: "Как сделать модальное окно на HTML, CSS и JS: элемент dialog, showModal(), открытие без JavaScript, закрытие по клику на фон и поддержка браузеров."
url: https://penlab.space/blog/ru/modal-dialog-html-css
language: ru
section: "Статьи"
published: 2026-10-12T15:56:50Z
updated: 2026-10-12T15:56:50Z
translation: https://penlab.space/blog/modal-dialog-html-css
---

# Как сделать модальное окно на 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()` по клику. Пока окно закрыто, браузер его не показывает.

```html
<button class="open" type="button">Открыть окно</button>

<dialog class="modal">
  <h2>Модальное окно</h2>
  <p>Страница под окном недоступна, пока оно открыто. Закрыть можно кнопкой или клавишей Esc.</p>
  <button class="close" type="button">Закрыть</button>
</dialog>
```

```js
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 он возвращается на кнопку «Открыть окно». Клик в точку, где под затемнением лежит кнопка «Открыть окно», до неё не доходит.

В живом примере окну уже заданы ширина, отступы и цвет затемнения. Эти стили разобраны в следующем разделе.

[Живой пример: Модальное окно на dialog: showModal() и close()](https://penlab.space/pen/EZ90e5Uh)

Под примером стоит ссылка на пен: он открывается в редакторе Penlab, и код в нём можно менять.

Открыть `dialog` можно тремя способами, и модальным окно получается только в одном:

- `showModal()` открывает модальное окно: с затемнением, блокировкой страницы и закрытием по Esc.
- `show()` открывает окно без затемнения и без блокировки страницы. Клавиша Esc такое окно не закрывает.
- Атрибут `open`, поставленный в разметке или добавленный из скрипта, даёт то же немодальное окно, что и `show()`.

Руками мы `open` не ставим и не снимаем. По спецификации HTML после снятия `open` у модального окна событие `close` не придёт, и страница останется заблокированной.

## Как оформить окно и затемнение

У `dialog` есть стили браузера: сплошная рамка, отступы в `1em`, центрирование через `margin: auto` и почти прозрачное затемнение. Обычно мы убираем рамку, задаём ширину и делаем фон `::backdrop` темнее.

```css
.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»](https://penlab.space/blog/ru/css-transition-display-none).

## Как открыть и закрыть окно без JavaScript

Кнопку связывают с элементом `dialog` атрибуты `commandfor` и `command`. В первый записываем `id` окна, во второй - действие: `show-modal` открывает окно как модальное, `close` закрывает его.

```html
<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 в нём нет.

[Живой пример: Модальное окно без JavaScript: commandfor и closedby](https://penlab.space/pen/ks3C4fsx)

Кнопке внутри формы нужен `type="button"`. Если кнопка с `commandfor` стоит внутри формы и `type` у неё не указан, команда не выполняется: в Chrome 154 такая кнопка не закрыла окно и не отправила форму.

В браузере без поддержки `commandfor` у кнопки нет действия: незнакомый атрибут браузер пропускает. В таком браузере мы пример не запускали, это следует из правил разбора HTML. Если на сайт заходят с таких браузеров, добавляем проверку и открываем окно из скрипта:

```js
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`.

```html
<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` и переносим отступы на блок внутри него, чтобы любой клик внутри окна попадал во вложенный элемент. В нашем примере этот блок - форма с кнопками окна.

```html
<dialog class="modal">
  <form method="dialog">
    <p>Содержимое окна</p>
    <button>Закрыть</button>
  </form>
</dialog>
```

```css
.modal {
  padding: 0;
}

.modal form {
  padding: 24px;
}
```

```js
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`.

```html
<dialog class="modal">
  <form method="dialog">
    <h2>Удалить черновик?</h2>
    <button value="cancel">Отмена</button>
    <button value="delete">Удалить</button>
  </form>
</dialog>
```

```js
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, который оставил прошлое значение. Чтобы результат не зависел от браузера и способа закрытия, перед каждым открытием свойство очищаем:

```js
modal.returnValue = '';
modal.showModal();
```

## Куда поставить фокус и как дать окну имя

Без подсказок браузер ставит фокус на первый элемент окна, до которого можно дойти клавишей Tab. Нужный элемент мы отмечаем атрибутом `autofocus`. В окне подтверждения это кнопка отмены: случайное нажатие Enter ничего не удалит.

```html
<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`.

```css
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`.

```html
<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>
```

```css
.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;
}
```

```js
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();
    }
  });
}
```

[Живой пример: Окно подтверждения на dialog: ответ пользователя в returnValue](https://penlab.space/pen/FCXHDgMx)

На этом пене можно проверить, зачем нужна очистка `returnValue`. Открываем его в редакторе Penlab по ссылке под примером и повторяем шаги:

1. Убираем из кода строку `modal.returnValue = '';`.
2. Нажимаем «Удалить черновик», затем «Удалить».
3. Открываем окно снова.
4. Кликаем по затемнению: на странице остаётся «Черновик удалён.», хотя в этот раз мы отказались. Если вернуть строку из шага 1, тот же клик меняет надпись на «Удаление отменено.».

Своё окно с нуля удобно собрать в [новом пене](https://penlab.space/new). Готовый пен можно отправить ссылкой или встроить на свою страницу. Об этом есть заметка [«Поделиться пеном и встроить его на свой сайт»](https://penlab.space/blog/ru/share-and-embed).

## 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»](https://penlab.space/blog/ru/css-transition-display-none).

### Можно ли запретить закрытие по Esc?

Да, атрибутом `closedby="none"` в браузерах, которые его поддерживают. В Chrome 154 окно с этим значением не закрылось ни по Esc, ни по клику на затемнение. Второй способ - отменить событие `cancel` через `preventDefault()`, но по спецификации отмена срабатывает не всегда: если между нажатиями Esc пользователь ничего не делал на странице, браузер вправе закрыть окно. Мы бы оставляли Esc рабочим везде, где закрытие окна ничего не ломает.

### Можно ли положить в окно форму с полями?

Да. Форма с `method="dialog"` данные на сервер не отправляет: она закрывает окно, а значения полей остаются в форме, и их можно прочитать в обработчике события `close`. В Chrome 154 поле сохранило введённый текст после закрытия окна такой формой.

### Почему подсказка или уведомление оказывается под окном?

Модальное окно рисуется в верхнем слое, и `z-index` остальных элементов страницы на этот слой не влияет. Всё за пределами окна к тому же инертно: не отвечает на клики и клавишу Tab. Уведомление, которое должно быть видно поверх окна, размещаем внутри самого `dialog`.

## Источники

- [HTML Standard: элемент dialog](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element)
- [MDN: элемент dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog)
- [Chrome for Developers: Introducing command and commandfor](https://developer.chrome.com/blog/command-and-commandfor)
- [Stack Overflow: как закрыть dialog кликом мимо окна](https://stackoverflow.com/questions/25864259)
- [WebKit: Release Notes for Safari Technology Preview 242](https://webkit.org/blog/17934/release-notes-for-safari-technology-preview-242/)
- [HTML Standard: атрибуты command и commandfor](https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-command)
- [MDN: HTMLDialogElement.showModal()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal)
- [W3C APG: Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
- [Chrome for Developers: New in Chrome 134](https://developer.chrome.com/blog/new-in-chrome-134)
- [Stack Overflow: кнопка закрытия dialog не работает, ответ про display](https://stackoverflow.com/questions/60365510)
- [HTML Standard: стили браузера для dialog](https://html.spec.whatwg.org/multipage/rendering.html#flow-content-3)
- [MDN: HTMLDialogElement.returnValue](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue)
- [WebKit: Introducing the Dialog Element](https://webkit.org/blog/12209/introducing-the-dialog-element/)
- [W3C: ARIA in HTML](https://www.w3.org/TR/html-aria/)
- [whatwg/html, issue 7732: прокрутка страницы под модальным окном](https://github.com/whatwg/html/issues/7732)
- [MDN: таблицы совместимости browser-compat-data](https://github.com/mdn/browser-compat-data)
- [Hidde de Vries: Dialogs and popovers seem similar. How are they different?](https://hidde.blog/dialog-modal-popover-differences/)
- [Stack Overflow: returnValue сохраняет прошлое значение](https://stackoverflow.com/questions/79773171)
