Как сделать лайтбокс с клавиатурной навигацией на JavaScript
Лайтбокс — это интерфейс, в котором изображение открывается поверх текущей страницы, а остальное содержимое временно перекрывается затемнённым фоном. Такой просмотрщик удобен для галерей, портфолио, каталогов и страниц с техническими иллюстрациями. Однако красивого эффекта недостаточно: пользователь должен иметь возможность закрыть окно, перейти к следующей фотографии и вернуться к предыдущей без мыши. Подробнее о 自作pcでケース内部のケーブルを結束バンドでまとめてホコリの溜まりにくいレイアウトを実現するコツ.
Клавиатурная навигация делает JavaScript-галерею доступнее и предсказуемее. Для этого понадобятся корректная HTML-разметка, несколько CSS-правил и обработчик событий keydown. Ниже разберём вариант без сторонних библиотек, который можно встроить в обычную WordPress-тему или статическую страницу.
Подготовка разметки галереи
Каждое миниатюрное изображение удобно оформить ссылкой. Атрибут href будет содержать адрес полноразмерного файла, а alt — его текстовое описание. Ссылка сохраняет привычное поведение браузера и остаётся доступной, если JavaScript отключён.
<div class="gallery" aria-label="Фотогалерея">
<a class="gallery__link"
href="images/photo-1-large.jpg"
data-title="Рабочее место">
<img src="images/photo-1-small.jpg" alt="Рабочее место с компьютером">
</a>
<a class="gallery__link"
href="images/photo-2-large.jpg"
data-title="Системный блок">
<img src="images/photo-2-small.jpg" alt="Системный блок на столе">
</a>
<a class="gallery__link"
href="images/photo-3-large.jpg"
data-title="Монитор">
<img src="images/photo-3-small.jpg" alt="Монитор с открытым редактором">
</a>
</div>
<div class="lightbox" id="lightbox" aria-hidden="true">
<div class="lightbox__backdrop" data-close></div>
<div class="lightbox__dialog"
role="dialog"
aria-modal="true"
aria-labelledby="lightbox-title">
<button class="lightbox__close" type="button" aria-label="Закрыть">
×
</button>
<button class="lightbox__prev" type="button" aria-label="Предыдущее изображение">
‹
</button>
<figure>
<img class="lightbox__image" src="" alt="">
<figcaption id="lightbox-title"></figcaption>
</figure>
<button class="lightbox__next" type="button" aria-label="Следующее изображение">
›
</button>
</div>
</div>
Атрибут aria-hidden="true" сообщает вспомогательным технологиям, что закрытая область не должна участвовать в навигации. Значение изменится на false после открытия. Роль dialog и свойство aria-modal="true" обозначают модальное окно, а подпись связывается с заголовком через aria-labelledby.
Такой подход похож на аккуратную организацию элементов в компьютерном корпусе: продуманная укладка кабелей упрощает обслуживание, а понятная структура DOM облегчает дальнейшую настройку скрипта. Не стоит смешивать миниатюры, кнопки управления и полноразмерное изображение в одном контейнере без определённых классов.
Стили для затемнения и адаптивного окна
По умолчанию лайтбокс лучше скрыть через display: none. После открытия ему добавляется класс is-open. Затемнение занимает весь экран, а диалог располагается поверх него. Свойство z-index нужно применять к слою целиком, чтобы окно не оказалось под шапкой сайта или другим фиксированным блоком.
.lightbox {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
}
.lightbox.is-open {
display: block;
}
.lightbox__backdrop {
position: absolute;
inset: 0;
background: rgb(0 0 0 / 80%);
}
.lightbox__dialog {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: min(92vw, 1100px);
height: 100%;
margin: 0 auto;
padding: 40px 60px;
}
.lightbox__image {
display: block;
max-width: 100%;
max-height: 82vh;
object-fit: contain;
}
.lightbox figure {
margin: 0;
text-align: center;
}
.lightbox figcaption {
margin-top: 12px;
color: #fff;
}
.lightbox button {
position: absolute;
border: 0;
color: #fff;
background: rgb(0 0 0 / 45%);
cursor: pointer;
}
.lightbox__close {
top: 16px;
right: 20px;
width: 42px;
height: 42px;
font-size: 30px;
}
.lightbox__prev,
.lightbox__next {
top: 50%;
width: 44px;
height: 60px;
transform: translateY(-50%);
font-size: 34px;
}
.lightbox__prev {
left: 8px;
}
.lightbox__next {
right: 8px;
}
.lightbox button:focus-visible {
outline: 3px solid #ffd54a;
outline-offset: 3px;
}
@media (max-width: 600px) {
.lightbox__dialog {
width: 100vw;
padding: 50px 42px 30px;
}
.lightbox__image {
max-height: 72vh;
}
}
max-width и max-height не позволяют фотографии выйти за пределы экрана. Это особенно важно на смартфонах, где вертикального пространства мало. Кнопки должны иметь заметное состояние :focus-visible, иначе пользователь, перемещающийся клавишей Tab, не поймёт, какой элемент выбран.
Для предотвращения прокрутки страницы при открытом окне можно временно добавлять классу body свойство overflow: hidden. При закрытии оно удаляется. Если на сайте уже используются собственные правила прокрутки, это изменение лучше делать через отдельный класс, а не напрямую менять встроенный стиль элемента.
Логика открытия и переключения изображений
Скрипт сначала собирает все ссылки галереи в массивоподобную коллекцию и хранит индекс текущего изображения. Функция showImage() получает нужный элемент, меняет адрес картинки, текст alt и подпись. Операции выполняются в одном месте, поэтому стрелки, клавиши и клики используют одинаковую логику.
const links = [...document.querySelectorAll('.gallery__link')];
const lightbox = document.querySelector('#lightbox');
const image = lightbox.querySelector('.lightbox__image');
const title = lightbox.querySelector('#lightbox-title');
const closeButton = lightbox.querySelector('.lightbox__close');
const previousButton = lightbox.querySelector('.lightbox__prev');
const nextButton = lightbox.querySelector('.lightbox__next');
let currentIndex = 0;
let lastFocusedElement = null;
function showImage(index) {
currentIndex = (index + links.length) % links.length;
const link = links[currentIndex];
const thumbnail = link.querySelector('img');
image.src = link.href;
image.alt = thumbnail.alt;
title.textContent = link.dataset.title || thumbnail.alt;
}
function openLightbox(index) {
lastFocusedElement = document.activeElement;
showImage(index);
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
document.body.classList.add('lightbox-open');
closeButton.focus();
}
function closeLightbox() {
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
document.body.classList.remove('lightbox-open');
image.src = '';
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
links.forEach((link, index) => {
link.addEventListener('click', (event) => {
event.preventDefault();
openLightbox(index);
});
});
previousButton.addEventListener('click', () => {
showImage(currentIndex - 1);
});
nextButton.addEventListener('click', () => {
showImage(currentIndex + 1);
});
closeButton.addEventListener('click', closeLightbox);
Формула (index + links.length) % links.length обеспечивает циклическую навигацию. После последнего изображения пользователь снова увидит первое, а перед первым — последнее. Если циклическое поведение не подходит для конкретной страницы, кнопки можно скрывать на границах массива, но для фотогалереи непрерывный переход обычно удобнее.
В обработчике клика используется preventDefault(), потому что ссылка не должна открывать большой файл в отдельной странице. При отключённом JavaScript это ограничение исчезает: браузер перейдёт по адресу полноразмерного изображения обычным способом.
Обработка клавиш и возврат фокуса
Теперь добавим управление с клавиатуры. Клавиша Escape закрывает модальное окно, стрелки переключают фотографии, а Tab перемещает фокус между кнопками. Для небольшой галереи можно ограничить фокус внутри диалога простым обработчиком, чтобы он не уходил на элементы страницы под затемнённым слоем.
lightbox.addEventListener('click', (event) => {
if (event.target.matches('[data-close]')) {
closeLightbox();
}
});
document.addEventListener('keydown', (event) => {
if (!lightbox.classList.contains('is-open')) {
return;
}
if (event.key === 'Escape') {
closeLightbox();
return;
}
if (event.key === 'ArrowLeft') {
event.preventDefault();
showImage(currentIndex - 1);
}
if (event.key === 'ArrowRight') {
event.preventDefault();
showImage(currentIndex + 1);
}
if (event.key === 'Tab') {
const focusable = [
closeButton,
previousButton,
nextButton
];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
});
Перед открытием сохраняется элемент, на котором находился фокус. После закрытия он получает фокус снова. Это важная деталь: без неё посетитель окажется в начале страницы или потеряет место, с которого запускал просмотр. На практике такой возврат особенно заметен при использовании экранного диктора и клавиатуры.
Если в диалоге появятся дополнительные поля, кнопки загрузки или подписи, их тоже нужно включить в массив focusable. Для сложных интерфейсов можно использовать специализированную библиотеку управления фокусом, но для компактного лайтбокса собственного обработчика достаточно.
Проверка ошибок и интеграция с сайтом
Изображение может не загрузиться из-за неверного пути, удаления файла или проблем с сервером. Чтобы пользователь не видел пустое окно, добавьте обработчик error и понятное сообщение. Также полезно отключать кнопку перехода, если в галерее только один элемент, хотя стрелки можно оставить для единообразия интерфейса.
image.addEventListener('error', () => {
image.alt = 'Изображение временно недоступно';
title.textContent = 'Не удалось загрузить изображение';
});
Проверьте, что адреса полноразмерных файлов корректно работают на страницах WordPress, где путь может зависеть от папки загрузок или настроек темы. Скрипт лучше подключать после разметки галереи либо использовать атрибут defer. Названия классов не должны конфликтовать с уже установленными плагинами и стилями редактора.
Перед публикацией протестируйте сценарий без мыши: откройте изображение клавишей Enter, перейдите стрелками, нажмите Escape, несколько раз используйте Tab, затем снова откройте галерею. Проверьте масштабирование на телефоне, длинные подписи, медленную загрузку и отсутствие горизонтальной прокрутки. Дополнительные материалы по JavaScript и настройке сайтов можно найти в практических материалах, сопоставляя отдельные решения с особенностями своей темы.
Готовый лайтбокс должен сохранять смысл изображения через alt, иметь видимый фокус, закрываться по клавише Escape и возвращать пользователя к исходной кнопке. Эти небольшие детали превращают декоративный эффект в полноценный компонент интерфейса. Сохраните разметку, стили и скрипт в отдельных файлах, подключите их к тестовой странице и постепенно адаптируйте размеры, цвета и анимацию под дизайн сайта.
Добавьте этот вариант в тестовую галерею, проверьте работу мышью, клавиатурой и на мобильном экране, а затем перенесите проверенный компонент на рабочие страницы.