Popup модальное окно
Зачем нужно
Модальное окно (modal) -- один из самых частых UI-паттернов. Оно блокирует взаимодействие с основным контентом и фокусирует внимание пользователя на конкретной задаче: подтверждение действия, форма, уведомление. Правильная реализация включает управление фокусом, доступность и блокировку скролла.
Где используется
- Подтверждение удаления ("Вы уверены?")
- Формы авторизации / регистрации
- Просмотр изображений (lightbox)
- Уведомления и предупреждения
- Мастеры (wizard) по шагам
Подход 1: Нативный <dialog> (рекомендуемый)
HTML-элемент <dialog> -- встроенное решение браузера с поддержкой ::backdrop, фокус-трапа и клавиши Escape из коробки.
HTML-разметка
<button id="openBtn" type="button">Открыть модальное окно</button>
<dialog id="modal" class="modal">
<form method="dialog">
<h2 class="modal__title">Подтверждение</h2>
<p class="modal__body">Вы уверены, что хотите удалить этот элемент?</p>
<div class="modal__actions">
<button value="cancel" class="btn btn--secondary">Отмена</button>
<button value="confirm" class="btn btn--primary">Удалить</button>
</div>
</form>
</dialog>
CSS-стили
.modal {
border: none;
border-radius: 12px;
padding: 32px;
max-width: 480px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
/* Оверлей через ::backdrop */
.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
/* Анимация появления */
.modal[open] {
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal__title {
margin: 0 0 16px;
font-size: 1.25rem;
}
.modal__body {
margin: 0 0 24px;
color: #555;
}
.modal__actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
JavaScript
const modal = document.getElementById('modal');
const openBtn = document.getElementById('openBtn');
// Открытие модального окна (showModal блокирует фон)
openBtn.addEventListener('click', () => {
modal.showModal;
});
// Обработка результата
modal.addEventListener('close', () => {
if (modal.returnValue === 'confirm') {
console.log('Пользователь подтвердил действие');
} else {
console.log('Пользователь отменил');
}
});
// Закрытие по клику на backdrop
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.close('cancel');
}
});
Важно:
showModalавтоматически создаёт backdrop, блокирует Tab-навигацию за пределами диалога и закрывается по Escape.
Подход 2: Кастомное модальное окно
Когда нужна полная кастомизация или поддержка старых браузеров.
HTML-разметка
<div id="customModal" class="custom-modal" role="dialog" aria-modal="true"
aria-labelledby="modalTitle" hidden>
<div class="custom-modal__overlay"></div>
<div class="custom-modal__content">
<button class="custom-modal__close" aria-label="Закрыть">×</button>
<h2 id="modalTitle">Заголовок</h2>
<p>Контент модального окна.</p>
<button class="btn btn--primary" id="confirmBtn">OK</button>
</div>
</div>
CSS
.custom-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.custom-modal[hidden] {
display: none;
}
.custom-modal__overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.custom-modal__content {
position: relative;
background: #fff;
border-radius: 12px;
padding: 32px;
max-width: 480px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
animation: fadeIn 0.2s ease-out;
}
.custom-modal__close {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
color: #999;
}
Focus Trap (ловушка фокуса)
class Modal {
constructor(element) {
this.modal = element;
this.overlay = element.querySelector('.custom-modal__overlay');
this.closeBtn = element.querySelector('.custom-modal__close');
this.previouslyFocused = null;
this.closeBtn.addEventListener('click', () => this.close());
this.overlay.addEventListener('click', () => this.close());
document.addEventListener('keydown', (e) => this.handleKeydown(e));
}
open {
this.previouslyFocused = document.activeElement;
this.modal.hidden = false;
document.body.style.overflow = 'hidden'; // Блокировка скролла
// Установить фокус на первый интерактивный элемент
const focusable = this.getFocusableElements;
if (focusable.length) focusable[0].focus();
}
close {
this.modal.hidden = true;
document.body.style.overflow = '';
// Вернуть фокус на элемент, который вызвал модальное окно
if (this.previouslyFocused) {
this.previouslyFocused.focus();
}
}
handleKeydown(e) {
if (this.modal.hidden) return;
// Закрытие по Escape
if (e.key === 'Escape') {
this.close();
return;
}
// Focus trap: Tab циклически внутри модального окна
if (e.key === 'Tab') {
const focusable = this.getFocusableElements;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
getFocusableElements {
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
return [...this.modal.querySelectorAll(selector)].filter(
(el) => !el.disabled && !el.hidden
);
}
}
// Использование
const modal = new Modal(document.getElementById('customModal'));
document.getElementById('openBtn').addEventListener('click', () => modal.open());
Блокировка скролла (scrollbar lock)
При открытии модального окна страница не должна прокручиваться. Но простое overflow: hidden вызывает сдвиг контента из-за исчезновения полосы прокрутки.
function lockScroll() {
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${scrollbarWidth}px`;
}
function unlockScroll() {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}
Accessibility-чеклист
| Атрибут | Назначение |
|---|---|
role="dialog" |
Сообщает скринридеру, что это диалог |
aria-modal="true" |
Контент за пределами диалога недоступен |
aria-labelledby |
Связывает с заголовком диалога |
aria-describedby |
Связывает с описанием (опционально) |
| Escape | Закрывает модальное окно |
| Focus trap | Tab не выходит за пределы диалога |
| Возврат фокуса | После закрытия фокус возвращается на триггер |
Частые ошибки
| Ошибка | Проблема | Решение |
|---|---|---|
| Нет focus trap | Tab уходит за модальное окно | Реализуй ловушку фокуса |
show вместо showModal |
Нет backdrop и Escape | Используй showModal |
| Скролл страницы при открытом модале | Пользователь теряется | overflow: hidden на body |
Нет aria-modal |
Скринридер читает фон | Добавь aria-modal="true" |
| Сдвиг контента при блокировке скролла | Полоса прокрутки исчезает | Компенсируй paddingRight |
Связанные темы
- Dropdown меню -- другой паттерн показа/скрытия
- Tooltip -- неблокирующая подсказка
- Drag and Drop -- взаимодействие внутри модальных окон
- Валидация формы в реальном времени -- формы внутри модала