Как сверстать модальное окно
Эта статья основана на одном из практических заданий verstaem.online.
Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.
Перейти к заданиюВ этой статье мы создадим модальное окно с затемнением фона и плавным открытием. На его примере разберем, как организовать HTML-разметку, оформить интерфейс с помощью CSS и реализовать открытие и закрытие модального окна через JavaScript.
Создаем HTML-разметку
Как обычно, начнем с базовой HTML-разметки. Для этого достаточно воспользоваться Emmet: вводим ! и нажимаем Tab.
Обратите внимание на атрибут lang. Поскольку содержимое страницы написано на русском языке, указываем значение ru.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Модальное окно</title>
</head>
<body>
</body>
</html>
Подключаем стили и JavaScript. Скрипт подключаем с атрибутом defer, чтобы он выполнился только после построения HTML-дерева.
<head>
...
<link rel="stylesheet" href="css/style.css">
<script src="js/script.js" defer></script>
</head>
Теперь создадим основной блок страницы. Именно здесь будет располагаться контент, поверх которого позже появится модальное окно.
<div class="content">
...
</div>
Внутри находятся заголовок, описание и кнопка открытия модального окна.
<h1 class="content__title">
Atelier No. 9
</h1>
<p class="content__descr">
Частные презентации модных коллекций и лимитированных капсульных коллекций по предварительной записи.
</p>
<button class="content__btn" data-path="modal-1">Записаться на примерку</button>
Обратите внимание на атрибут data-path. Именно он позволит связать кнопку с конкретным модальным окном. Такой подход удобен тем, что в будущем на странице может появиться сразу несколько модальных окон, и весь JavaScript продолжит работать без изменений.
Теперь создадим обертку модального окна.
<div
class="modal-wrapper"
data-modal-wrapper
hidden>
</div>
Именно этот блок отвечает за затемнение страницы. Внутри него располагается само модальное окно.
<div class="modal" role="dialog" aria-modal="true" data-target="modal-1" hidden>
<button class="modal__close" data-modal-close aria-label="Закрыть окно">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 5L5 15" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5 5L15 15" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="modal__top">Private Event</div>
<h2 class="modal__title">
Подтвердить бронирование?
</h2>
<p class="modal__descr">
Место будет закреплено на 20 минут. После подтверждения приглашение и QR-пропуск появятся в личном кабинете.
</p>
<div class="modal__card">
<div class="modal__text">
<h3 class="modal__subtitle">
Evening Preview
</h3>
<time datetime="2026-28-07T19:30:00" class="modal__time">28 июля · 19:30</time>
</div>
<div class="modal__guests">
2 гостя
</div>
</div>
<div class="modal__btns">
<button class="modal__btn">Позже</button>
<button class="modal__btn modal__btn--approve">Подтвердить</button>
</div>
</div>
Обратите внимание сразу на несколько моментов. Во-первых, используется role="dialog". Этот атрибут сообщает вспомогательным технологиям, что перед ними именно диалоговое окно. Во-вторых, добавлен атрибут aria-modal="true". Он говорит браузеру, что пока открыто это окно, пользователь взаимодействует только с ним. В-третьих, используется data-target. Его значение совпадает со значением data-path у кнопки открытия.
Обратите внимание на дополнительный класс у второй кнопки. Он нужен, чтобы изменить ее внешний вид так, как нам нужно по макету.
Добавляем стили
Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифты, который используется в макете.
* {
box-sizing: border-box;
}
@font-face {
font-family: "Golos Text";
src: url("../fonts/GolosTextRegular.woff2");
font-display: swap;
font-weight: 400;
}
@font-face {
font-family: "Golos Text";
src: url("../fonts/GolosTextBold.woff2");
font-display: swap;
font-weight: 700;
}
@font-face {
font-family: "Cormorant Garamond";
src: url("../fonts/CormorantGaramondBold.woff2");
font-display: swap;
font-weight: 700;
}
body {
margin: 0;
padding: 80px 20px;
background: #1f1425;
font-family: "Golos Text", sans-serif;
}
.no-scroll {
overflow: hidden;
}
Здесь мы также сразу добавляем класс, который позволит «отключить» скролл у body.
Теперь оформим основной контент. Он представляет собой обычную карточку с эффектом размытия.
.content {
margin: 0 auto;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 32px;
padding: 56px;
max-width: 780px;
backdrop-filter: blur(36px);
background: rgba(255, 255, 255, 0.03);
}
.content__title {
margin: 0;
margin-bottom: 16px;
font-family: "Cormorant Garamond", sans-serif;
font-weight: 700;
font-size: 64px;
line-height: 120%;
color: #f7efe7;
}
.content__descr {
margin: 0;
margin-bottom: 32px;
max-width: 420px;
font-weight: 400;
font-size: 16px;
line-height: 140%;
color: #d6cbc3;
}
.content__btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 100px;
padding: 16px 32px;
font-family: inherit;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #d6cbc3;
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.06);
cursor: pointer;
transition: background 0.3s;
}
.content__btn:hover {
background: rgba(255, 255, 255, 0.35);
}
Не забывайте про добавление :hover к кнопкам.
Далее переходим к затемнению страницы. Используем фиксированное позиционирование, чтобы обертка занимала весь экран.
.modal-wrapper {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
padding: 40px 0;
cursor: pointer;
overflow-y: auto;
overflow-x: hidden;
opacity: 0;
visibility: hidden;
}
Обратите внимание на overflow-y: auto. Если высота модального окна окажется больше высоты экрана, пользователь сможет прокрутить именно его, а не всю страницу.
По умолчанию окно должно быть скрыто. Поэтому добавляем прозрачность и скрываем видимость через opacity: 0; visibility: hidden;. Когда JavaScript добавит класс открытия, просто возвращаем значения обратно.
.modal-wrapper--open {
opacity: 1;
visibility: visible;
}
Далее само модальное окно. Стили простые, но обратите внимание на то, как оно центрировано. Поскольку у родителя используется flexbox, мы задаем модалке margin: auto, и тем самым отступам по равной доле с каждой стороны, в итоге окно по центру экрана в любом случае.
.modal {
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
width: calc(100% - 20px);
max-width: 542px;
margin: auto;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 32px;
padding: 32px;
backdrop-filter: blur(32px);
box-shadow: 0 30px 90px 0 rgba(0, 0, 0, 0.35);
background: rgba(255, 245, 236, 0.14);
cursor: default;
}
Также обратите внимание на свойство cursor. У обертки окна мы делали pointer, чтобы пользователь мог понять, что элемент кликабельный, а на самом окне делаем default, т. к. оно само по себе не кликабельно, кликабельны лишь кнопки внутри.
Далее стилизуем кнопку закрытия. Используем абсолютное позиционирование.
.modal__close {
position: absolute;
right: 20px;
top: 20px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 100px;
width: 36px;
height: 36px;
color: #fff;
background: rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: background 0.3s, color 0.3s;
}
.modal__close svg {
display: block;
width: 20px;
height: 20px;
stroke: currentColor;
}
.modal__close:hover {
color: #2b1f18;
background: #f3d2b2;
}
Обратите внимание на SVG. У него используется currentColor. Благодаря этому цвет крестика автоматически наследуется от свойства color, поэтому менять оформление при наведении становится значительно проще.
Остальные элементы делаются просто по макету, поэтому просто покажу код.
.modal__top {
font-weight: 400;
font-size: 12px;
line-height: 120%;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #f3d2b2;
}
.modal__title {
margin: 0;
font-family: "Cormorant Garamond", sans-serif;
font-weight: 700;
font-size: 44px;
line-height: 120%;
color: #fff8f2;
}
.modal__descr {
margin: 0;
font-weight: 400;
font-size: 16px;
line-height: 140%;
color: rgba(242, 231, 223, 0.8);
}
.modal__card {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 20px;
padding: 16px;
background: rgba(255, 255, 255, 0.08);
}
.modal__text {
display: flex;
flex-direction: column;
gap: 4px;
}
.modal__subtitle {
margin: 0;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #fff8f2;
}
.modal__time {
font-weight: 400;
font-size: 12px;
line-height: 120%;
color: rgba(255, 255, 255, 0.75);
}
.modal__guests {
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #fff8f2;
}
.modal__btns {
display: flex;
gap: 12px;
}
.modal__btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
flex: 1;
border-radius: 16px;
padding: 16px;
font-family: inherit;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #fff;
cursor: pointer;
background: rgba(255, 255, 255, 0.12);
transition: background 0.3s, color 0.3s;
}
.modal__btn:hover {
color: #2b1f18;
background: #f3d2b2;
}
.modal__btn--approve {
color: #2b1f18;
background: #f3d2b2;
}
.modal__btn--approve:hover {
color: #fff;
background: rgba(255, 255, 255, 0.12);
}
Из необычного здесь можно выделить разве что добавление шрифта «Cormorant Garamond» для определенных элементов. Не забывайте делать это, так как у основного контента страницы шрифт другой.
Добавляем JavaScript
Переходим к логике открытия окна. Сначала получаем необходимые элементы.
const wrapper = document.querySelector('[data-modal-wrapper]');
const openBtns = document.querySelectorAll('[data-path]');
const closeBtns = document.querySelectorAll('[data-modal-close]');
Создадим функцию открытия. Она получает название модального окна и ищет его по атрибуту data-target.
function openModal(target) {
}
После нахождения окна выполняем три действия.
wrapper.classList.add('modal-wrapper--open');
modal.removeAttribute('hidden');
document.body.classList.add('no-scroll');
Мы:
- показываем затемнение;
- отображаем окно;
- запрещаем прокрутку страницы.
Теперь создадим функцию закрытия.
function closeModal() {
}
Здесь все происходит в обратном порядке.
wrapper.classList.remove('modal-wrapper--open');
activeModal.setAttribute('hidden', '');
document.body.classList.remove('no-scroll');
Остается только назначить обработчики событий. Сначала — кнопки открытия.
openBtns.forEach(btn => {
btn.addEventListener('click', () => {
openModal(btn.dataset.path);
});
});
Затем кнопки закрытия.
closeBtns.forEach(btn => {
btn.addEventListener('click', closeModal);
});
После этого реализуем закрытие по клику на затемнение. Обратите внимание, что проверяется именно элемент-обертка. Если клик будет на другое место — он не сработает.
wrapper.addEventListener('click', e => {
if (e.target === wrapper) {
closeModal();
}
});
Последний штрих — закрытие по клавише Escape. Это хороший тон — позволять пользователю работать не только мышкой, но и клавиатурой.
document.addEventListener('keydown', e => {
if (
e.key === 'Escape' &&
wrapper.classList.contains('modal-wrapper--open')
) {
closeModal();
}
});
Теперь у нас получилось полноценное модальное окно с затемнением страницы, блокировкой прокрутки, несколькими вариантами закрытия и возможностью легко добавить новые модальные окна без изменения JavaScript.