Реализация галереи изображений (Lightbox) на сайте
Галерея с lightbox — один из самых частых UI-компонентов. Задача кажется простой, пока не сталкиваешься с touch-свайпами, lazy-загрузкой, zoom, предзагрузкой соседних изображений и доступностью.
Библиотеки
PhotoSwipe 5 — лучший вариант для большинства задач. Чистый vanilla JS, touch-first, zoom, поддержка srcset. Активно поддерживается, ~20 KB gzip.
GLightbox — легковесный (~12 KB), поддерживает видео и iframe в лайтбоксе, без зависимостей.
Fancybox 5 — богатый функционал, но платный для коммерческих проектов.
Для простых случаев без zoom и видео — GLightbox. Для полноценной фотогалереи — PhotoSwipe.
PhotoSwipe: интеграция
npm install photoswipe
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,
})
// Кастомный caption
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
// Генерируем srcset для лайтбокса — разные размеры для разных экранов
function buildSrcSet(baseUrl: string, sizes: number[]): string {
return sizes.map(w => `${baseUrl}?w=${w} ${w}w`).join(', ')
}
// В PhotoSwipe 5 — через dataSource
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,
// Для мобильных — не грузить 2560px
msrc: img.thumbnail, // placeholder до загрузки полного
}))
Masonry-лейаут сетки
/* CSS Columns — простейшее masonry без JS */
.photo-gallery {
columns: 3 200px;
column-gap: 8px;
}
.photo-gallery__item {
break-inside: avoid;
margin-bottom: 8px;
}
.photo-gallery__item img {
width: 100%;
height: auto;
display: block;
}
@media (max-width: 768px) {
.photo-gallery { columns: 2 150px; }
}
@media (max-width: 480px) {
.photo-gallery { columns: 1; }
}
Для поддержки CSS Masonry (пока только в Firefox за флагом) или точного выравнивания рядов — Masonry.js или native CSS grid с grid-template-rows: masonry.
Равномерная сетка с known dimensions
/* Если соотношение сторон известно заранее */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
}
.gallery-grid__item {
aspect-ratio: 4/3;
overflow: hidden;
}
.gallery-grid__item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.gallery-grid__item:hover img {
transform: scale(1.05);
}
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' : ''}`}>
{/* LQIP (Low Quality Image Placeholder) */}
{!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>
)
}
GLightbox для видео-галереи
npm install glightbox
import GLightbox from 'glightbox'
import 'glightbox/dist/css/glightbox.css'
const lightbox = GLightbox({
elements: [
{
href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
type: 'video',
source: 'youtube',
width: 900,
},
{
href: '/video/promo.mp4',
type: 'video',
description: 'Промо-ролик',
},
{
href: '/images/photo1.jpg',
type: 'image',
description: 'Описание фото',
},
],
autoplayVideos: false,
loop: false,
draggable: true,
dragToleranceX: 40,
dragToleranceY: 65,
swipeToClose: true,
})
Keyboard navigation и accessibility
// PhotoSwipe поддерживает клавиатуру нативно
// Дополнительно: возврат фокуса при закрытии
lightbox.on('close', () => {
// Возвращаем фокус на тот элемент, с которого открыли
const opener = document.querySelector('[data-gallery-opener]') as HTMLElement
opener?.focus()
})
// ARIA для сетки
// role="list" + role="listitem" или просто нативный <ul><li>
<ul class="photo-gallery" role="list" aria-label="Галерея проектов">
<li role="listitem">
<a href="/large/1.jpg"
aria-label="Открыть изображение: Проект офиса, 2024"
data-pswp-width="2400"
data-pswp-height="1600">
<img src="/thumb/1.jpg" alt="Проект офиса, 2024" loading="lazy">
</a>
</li>
</ul>
Сроки
GLightbox с базовой сеткой и CSS columns — половина дня. PhotoSwipe с masonry, srcset, caption, и кастомным UI — 1.5–2 дня. Полноценная галерея с загрузкой из API, пагинацией, фильтрами по категориям и видео поддержкой — 4–5 дней.







