Ми стикалися з ситуацією, коли клік по мініатюрі відкриває порожній екран або галерея гальмує на мобільних. Типові помилки — відсутність 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 і гарантуємо якість. Отримайте консультацію — підберемо оптимальне рішення під ваш стек.







