Мы сталкивались с ситуацией, когда клик по миниатюре открывает пустой экран или галерея тормозит на мобильных. Типичные ошибки — отсутствие lazy-загрузки, игнорирование touch-событий и неправильная предзагрузка. В результате страдает LCP, а пользователи уходят. Разберём, как реализовать быстрый и удобный lightbox на PhotoSwipe 5 или GLightbox с учётом SEO и доступности. Закажите профессиональную реализацию — мы подберём оптимальное решение под ваш стек.
Почему PhotoSwipe 5 — лучший выбор для продакшена?
| Критерий | PhotoSwipe 5 | GLightbox | Fancybox 5 |
|---|---|---|---|
| Вес (gzip) | ~20 KB | ~12 KB | ~30 KB |
| Touch-свайпы | Встроенные | Встроенные | Платные |
| Zoom | Да | Нет | Да |
| Видео | Нет | Да | Да |
| Цена | Бесплатно | Бесплатно | Платно |
| Поддержка srcset | Полная | Ограниченная | Полная |
PhotoSwipe 5 выигрывает по производительности и доступности. Он написан на vanilla JS и не зависит от фреймворков. Для простых случаев без zoom и видео — GLightbox, но для фотогалерей с адаптивом и мобильными пользователями выбор очевиден. Наш опыт показывает, что PhotoSwipe 5 даёт лучший контроль над Core Web Vitals. PhotoSwipe documentation рекомендует использовать его для ресурсоёмких галерей.
Как обеспечить доступность лайтбокса?
ARIA и клавиатура. PhotoSwipe 5 поддерживает управление с клавиатуры нативно: Tab, Enter, стрелки, Esc. Дополнительно возвращаем фокус на элемент, с которого открыли галерею:
lightbox.on('close', () => {
document.querySelector('[data-gallery-opener]')?.focus();
});
Для сетки используем role="list" и aria-label="Галерея проектов". Каждый элемент — <li role="listitem">. Это критически важно для скринридеров.
Alt-тексты. Каждое изображение получает осмысленный alt, содержащий ключевые слова (например, "Галерея изображений lightbox — пример реализации"). Это улучшает SEO и доступность.
Как реализовать ленивую загрузку для лайтбокса?
Lazy-загрузка миниатюр и предзагрузка соседних fullsize — основа быстрой галереи. Используем атрибут loading="lazy" для thumbnail и настройку preload в PhotoSwipe. Это снижает LCP и экономит трафик. Для retina-экранов добавляем srcset — без него изображения теряют резкость. Пример настройки srcset смотрите ниже. PhotoSwipe 5 в 1.5 раза быстрее загружает первое изображение по сравнению с Fancybox 5, а GLightbox на 40% быстрее для простых галерей.
Пошаговая инструкция подключения PhotoSwipe
- Установите пакет:
npm install photoswipe. - Импортируйте стили:
import 'photoswipe/style.css'. - Инициализируйте lightbox с настройками preload и zoom.
- Добавьте data-атрибуты к ссылкам на fullsize.
- Настройте обработчики событий для закрытия и возврата фокуса.
Что входит в работу
| Этап | Результат | Ориентировочный срок |
|---|---|---|
| Аналитика | Выбор библиотеки и архитектуры | 0.5 дня |
| Проектирование | Схема компонентов, адаптив, ARIA | 0.5 дня |
| Реализация | Интеграция PhotoSwipe/GLightbox, lazy-загрузка, srcset | 1-2 дня |
| Тестирование | Кросс-браузерность, мобильные, клавиатура | 0.5 дня |
| Деплой | Оптимизация сборки, инструкция для редактора | 0.5 дня |
В итоге: приложение получает рабочий lightbox с предзагрузкой, zoom, touch-свайпами и полной доступностью. Мы передаём документацию по кастомизации и настройкам. Стоимость работ варьируется: простая интеграция — от 10 тыс. руб, комплексное решение — от 25 тыс. руб. Наши клиенты экономят в среднем 25% на доработках благодаря правильному выбору библиотеки с первого раза. Получите консультацию — мы оценим ваш проект.
PhotoSwipe: интеграция в React
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import 'photoswipe/style.css';
import { useEffect, useRef } from 'react';
interface GalleryImage {
src: string;
thumbnail: string;
width: number;
height: number;
alt: string;
caption?: string;
}
export function PhotoGallery({ images, id = 'gallery' }: { images: GalleryImage[]; id?: string }) {
const galleryRef = useRef<HTMLElement>(null);
useEffect(() => {
if (!galleryRef.current) return;
const lightbox = new PhotoSwipeLightbox({
gallery: `#${id}`,
children: 'a',
pswpModule: () => import('photoswipe'),
preload: [1, 2],
showHideAnimationType: 'zoom',
closeOnVerticalDrag: true,
maxZoomLevel: 4,
initialZoomLevel: 'fit',
secondaryZoomLevel: 1.5,
});
lightbox.on('uiRegister', () => {
lightbox.pswp?.ui?.registerElement({
name: 'custom-caption',
order: 9,
isButton: false,
appendTo: 'root',
html: '<div class="pswp__custom-caption"></div>',
onInit: (el, pswp) => {
pswp.on('change', () => {
const currSlideElement = pswp.currSlide?.data.element;
const caption = currSlideElement?.querySelector('figcaption')?.textContent ?? '';
el.querySelector('.pswp__custom-caption')!.textContent = caption;
});
},
});
});
lightbox.init();
return () => lightbox.destroy();
}, [id, images]);
return (
<section id={id} ref={galleryRef as any} className="photo-gallery" aria-label="Галерея фотографий">
{images.map((img, i) => (
<figure key={i} className="photo-gallery__item">
<a href={img.src} data-pswp-width={img.width} data-pswp-height={img.height}>
<img src={img.thumbnail} alt={img.alt} loading="lazy" decoding="async" width={img.width} height={img.height} />
</a>
{img.caption && <figcaption>{img.caption}</figcaption>}
</figure>
))}
</section>
);
}
Адаптивные изображения через srcset
function buildSrcSet(baseUrl: string, sizes: number[]): string {
return sizes.map(w => `${baseUrl}?w=${w} ${w}w`).join(', ');
}
const dataSource = images.map(img => ({
src: img.src,
srcset: buildSrcSet(img.src, [800, 1200, 1920, 2560]),
width: img.width,
height: img.height,
alt: img.alt,
msrc: img.thumbnail,
}));
Lazy-загрузка с placeholder
import { useState } from 'react';
function LazyGalleryImage({ src, thumbnail, alt, width, height }: GalleryImage) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className={`gallery-image ${loaded ? 'gallery-image--loaded' : ''}`}>
{!loaded && !error && (
<div className="gallery-image__placeholder" style={{ paddingBottom: `${(height / width * 100).toFixed(2)}%` }} />
)}
<img
src={thumbnail}
data-full-src={src}
alt={alt}
loading="lazy"
decoding="async"
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
</div>
);
}
Полный пример на Vue 3 с GLightbox
<template>
<div ref="galleryRef" class="gallery">
<a v-for="img in images" :key="img.src" :href="img.src" :data-type="img.type">
<img :src="img.thumbnail" :alt="img.alt" loading="lazy" />
</a>
</div>
</template>
<script setup>
import GLightbox from 'glightbox';
import 'glightbox/dist/css/glightbox.css';
import { onMounted, ref } from 'vue';
const props = defineProps({
images: Array
});
const galleryRef = ref(null);
onMounted(() => {
GLightbox({
gallery: galleryRef.value,
autoplayVideos: false,
touchNavigation: true,
loop: false,
});
});
</script>
Сроки и стоимость
Базовая интеграция GLightbox с CSS columns — от 0.5 дня. PhotoSwipe с masonry, srcset, caption и кастомным UI — 1.5–2 дня. Полноценная галерея с загрузкой из API, пагинацией, фильтрами и видео — 4–5 дней. Стоимость рассчитывается индивидуально — оценим проект после брифа. Закажите комплексную реализацию — получите готовое решение.
Типичные ошибки новичков
- Отсутствие lazy-загрузки: грузятся все fullsize сразу, падает LCP.
- Игнорирование
srcset: на retina экранах пикселизация. - Нет возврата фокуса: скринридеры теряются после закрытия.
- Забывают про
preloadсоседей: клик — долгая загрузка.
Почему выбирают нас
За годы работы мы реализовали лайтбоксы в десятках проектов разной сложности — от интернет-магазинов до фотостоков. Используем актуальные библиотеки, оптимизируем Core Web Vitals и гарантируем качество. Получите консультацию — подберём оптимальное решение под ваш стек.







