Как создать всплывающее окно с изображением на JavaScript
Всплывающее окно с увеличенной фотографией часто встречается в галереях, каталогах товаров и портфолио. Пользователь нажимает на миниатюру, после чего поверх страницы появляется затемнённый слой с изображением. Такой элемент называют модальным окном, лайтбоксом или просмотрщиком изображений.
Для реализации не требуется подключать большой фреймворк. Достаточно HTML для разметки, CSS для оформления и нескольких обработчиков событий на JavaScript. При этом готовое решение можно сделать удобным для клавиатуры, быстрым на мобильных устройствах и безопасным с точки зрения вставки пользовательских данных.
В простом варианте окно открывается по клику, закрывается нажатием на крестик или фон, а картинка подстраивается под размер экрана. Дополнительные возможности — закрытие клавишей Escape, переключение между изображениями и плавная анимация появления.
Лучше сразу разделить задачу на независимые части: сначала создать скрытый контейнер, затем описать его внешний вид и только после этого добавить поведение. Такой подход облегчает отладку и позволяет изменять дизайн без переписывания скрипта.
Подготовка HTML-разметки
Для примера возьмём одну миниатюру. Атрибут data-full содержит путь к изображению большого размера, которое будет показано в модальном окне. Это удобнее, чем использовать одну и ту же картинку: миниатюра загружается быстрее, а полноразмерный файл открывается только по запросу.
<button class="gallery-item" type="button"
data-full="images/photo-large.jpg"
data-caption="Горный пейзаж">
<img src="images/photo-small.jpg"
alt="Горный пейзаж на закате">
</button>
<div class="lightbox" id="lightbox" aria-hidden="true">
<div class="lightbox__content" role="dialog"
aria-modal="true" aria-label="Просмотр изображения">
<button class="lightbox__close" type="button"
aria-label="Закрыть изображение">×</button>
<img class="lightbox__image" src="" alt="">
<p class="lightbox__caption"></p>
</div>
</div>
Кнопка вокруг миниатюры делает элемент доступным для клавиатуры без дополнительных обработчиков. Атрибуты aria-hidden, aria-modal и aria-label сообщают вспомогательным технологиям назначение блока. Если на странице несколько изображений, каждому элементу можно присвоить класс gallery-item.
Стили для затемнённого слоя
Модальное окно обычно фиксируется относительно окна браузера и занимает всю видимую область. Свойство z-index поднимает его над остальным содержимым, а display: none скрывает блок до момента открытия. Класс is-open будет добавляться скриптом.
.lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgb(0 0 0 / 78%);
opacity: 0;
transition: opacity 0.2s ease;
}
.lightbox.is-open {
display: flex;
opacity: 1;
}
.lightbox__content {
position: relative;
max-width: min(960px, 100%);
max-height: 100%;
}
.lightbox__image {
display: block;
max-width: 100%;
max-height: 85vh;
object-fit: contain;
border-radius: 6px;
}
.lightbox__close {
position: absolute;
top: -42px;
right: 0;
width: 36px;
height: 36px;
border: 0;
border-radius: 50%;
color: #fff;
background: #333;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.lightbox__caption {
margin: 10px 0 0;
color: #fff;
text-align: center;
}
max-height: 85vh не позволяет высокому изображению выйти за пределы экрана. На узких дисплеях отступы и размер картинки автоматически уменьшаются благодаря max-width: 100%. При необходимости анимацию можно дополнить изменением масштаба, однако слишком резкие эффекты ухудшают восприятие и могут мешать просмотру.
Логика открытия и закрытия
JavaScript получает ссылки на элементы, отслеживает нажатия на миниатюры и подставляет нужный адрес в свойство src. Значение alt также переносится из исходной картинки, чтобы увеличенная версия сохраняла текстовое описание.
const lightbox = document.querySelector('#lightbox');
const lightboxImage = lightbox.querySelector('.lightbox__image');
const lightboxCaption = lightbox.querySelector('.lightbox__caption');
const closeButton = lightbox.querySelector('.lightbox__close');
const galleryItems = document.querySelectorAll('.gallery-item');
let previousElement = null;
function openLightbox(item) {
const thumbnail = item.querySelector('img');
const fullImage = item.dataset.full;
const caption = item.dataset.caption || thumbnail.alt;
previousElement = document.activeElement;
lightboxImage.src = fullImage;
lightboxImage.alt = thumbnail.alt;
lightboxCaption.textContent = caption;
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
closeButton.focus();
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
lightboxImage.removeAttribute('src');
document.body.style.overflow = '';
if (previousElement) {
previousElement.focus();
}
}
galleryItems.forEach((item) => {
item.addEventListener('click', () => openLightbox(item));
});
closeButton.addEventListener('click', closeLightbox);
При открытии сохраняется активный элемент, чтобы после закрытия фокус вернулся к выбранной миниатюре. Блокировка прокрутки страницы предотвращает ситуацию, когда пользователь случайно перемещается по фону под окном. Удаление src после закрытия освобождает связь с большой картинкой, хотя уже загруженный браузером ресурс может оставаться в кэше.
Закрытие по фону и клавише
Пользователь ожидает, что модальное окно можно закрыть несколькими привычными способами. Кроме кнопки, обработаем щелчок по затемнённому фону и нажатие Escape. Проверка event.target === lightbox нужна для того, чтобы клик по самой картинке не закрывал просмотр.
lightbox.addEventListener('click', (event) => {
if (event.target === lightbox) {
closeLightbox();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && lightbox.classList.contains('is-open')) {
closeLightbox();
}
});
На сенсорных устройствах фон занимает всю область вокруг изображения, поэтому закрытие касанием работает естественно. Если внутри окна появятся дополнительные кнопки, обработчик останется корректным: события от них не будут ошибочно восприниматься как клик по фону.
При создании галереи из внешних данных не следует без проверки вставлять подписи через innerHTML. Для обычного текста безопаснее использовать textContent, как в примере. Это исключает выполнение HTML-кода, который может попасть в подпись из формы, базы данных или стороннего API.
Доступность и удобство просмотра
Кнопка закрытия должна иметь понятный aria-label, поскольку символ крестика сам по себе не всегда корректно озвучивается экранным диктором. У изображения должен оставаться содержательный alt, а пустое описание допустимо только для декоративных элементов.
Фокус желательно удерживать внутри открытого диалога. В небольшом примере достаточно автоматически переводить его на кнопку закрытия и возвращать обратно после завершения просмотра. В сложной галерее можно дополнительно реализовать переход фокуса между кнопками и запрет выхода на элементы страницы.
Размер исходного файла тоже влияет на удобство. Перед загрузкой стоит сжать изображения и подготовить несколько вариантов разрешения. Подобрать подходящий формат и выполнить базовую обработку можно через сервис для изображений, а на сайте оставить уменьшенные превью. Для фотографий обычно подходят JPEG и WebP, для прозрачной графики — PNG или WebP.
Варианты реализации
Небольшой скрипт без зависимостей подходит для одной галереи и не требует сборщика. Готовые библиотеки дают больше возможностей, но увеличивают объём страницы и требуют изучения их API. CSS-псевдоэлементы могут создать эффект затемнения, однако содержимое окна всё равно удобнее контролировать через отдельный HTML-контейнер.
| Подход | Преимущества | Ограничения | Когда выбрать |
|---|---|---|---|
| Чистый JavaScript | Полный контроль, нет зависимостей | Нужно самостоятельно учесть доступность | Для небольшого сайта или учебного проекта |
| Готовая библиотека | Галерея, анимации и навигация уже реализованы | Дополнительные файлы и настройки | Для большого каталога изображений |
| CSS без скрипта | Минимум кода | Ограниченное управление и слабая интерактивность | Для простого декоративного эффекта |
| Компонент фреймворка | Удобная интеграция с приложением | Требуется React, Vue или другой фреймворк | Для интерфейса на основе компонентов |
Если на странице уже используется WordPress, не обязательно подключать библиотеку только ради одного окна. Скрипт можно вынести в отдельный файл темы или небольшого плагина, а стили добавить через пользовательский CSS. При большом количестве функций библиотека становится оправданной, особенно когда нужны стрелки навигации, предварительная загрузка и жесты.
Производительность и безопасность
Большие фотографии желательно загружать лениво: миниатюры отображаются сразу, а оригинал запрашивается после клика. Для этого можно хранить адрес полноразмерного файла в data-full, как показано выше, или создавать его программно. Важно проверять, что адрес ведёт на допустимый ресурс вашего сайта, если он формируется из пользовательского ввода.
Не стоит разрешать произвольные значения в атрибутах без фильтрации. Особое внимание требуется при работе с внешними ссылками, SVG-файлами и данными из CMS. Надёжнее использовать заранее разрешённые пути, проверять MIME-тип изображений и применять Content Security Policy там, где это возможно.
Настройки приватности операционной системы не заменяют защиту сайта, однако помогают уменьшить объём фоновой телеметрии на рабочем компьютере. Дополнительные сведения о настройках приватности Windows пригодятся при настройке тестовой среды, особенно если разработка ведётся на Windows 11.
Готовое всплывающее окно следует проверить в Chrome, Firefox, Edge и мобильном Safari. Проверьте открытие по клавише Enter, закрытие через Escape, возврат фокуса, работу длинных подписей и поведение при медленном соединении. После такой проверки модальный просмотр изображений будет выглядеть аккуратно и останется удобным для разных категорий пользователей.
Сохраните этот пример как основу для своей галереи: добавьте несколько кнопок с атрибутом data-full, подключите стили и постепенно расширяйте функциональность. Так можно получить быстрый лайтбокс на JavaScript без лишних зависимостей и с полным контролем над разметкой.