We've seen it happen: a click on a thumbnail opens a blank screen, or the gallery stutters on mobile. Common mistakes include no lazy loading, ignoring touch events, and incorrect preloading. As a result, LCP suffers and users leave. Let's dive into how to implement a fast, user-friendly lightbox using PhotoSwipe 5 or GLightbox with SEO and accessibility in mind. With 5+ years of experience and over 200 gallery implementations, we deliver reliable solutions. Contact us for a professional implementation—we'll choose the optimal solution for your stack.
Why PhotoSwipe 5 is the Best Choice for Production
| Criteria | PhotoSwipe 5 | GLightbox | Fancybox 5 |
|---|---|---|---|
| Size (gzip) | ~20 KB | ~12 KB | ~30 KB |
| Touch swipes | Built-in | Built-in | Paid |
| Zoom | Yes | No | Yes |
| Video | No | Yes | Yes |
| Price | Free | Free | Paid |
| srcset support | Full | Limited | Full |
PhotoSwipe 5 wins on performance and accessibility. It's written in vanilla JS with no framework dependencies. For simple cases without zoom and video, GLightbox works, but for photo galleries with responsive design and mobile users, the choice is clear. Our experience shows that PhotoSwipe 5 gives the best control over Core Web Vitals. PhotoSwipe documentation recommends it for resource-intensive galleries.
Ensuring Accessibility and Performance
ARIA and Keyboard. PhotoSwipe 5 natively supports keyboard navigation: Tab, Enter, arrows, Esc. Additionally, we return focus to the element that opened the gallery:
lightbox.on('close', () => { document.querySelector('[data-gallery-opener]')?.focus(); }); For the grid, use role="list" and aria-label="Project gallery". Each item is <li role="listitem">. This is critical for screen readers.
Alt texts. Each image gets a meaningful alt containing keywords (e.g., "Lightbox gallery example with PhotoSwipe"). This improves SEO and accessibility.
Lazy Loading. Lazy loading thumbnails and preloading adjacent full-size images are the foundation of a fast gallery. Use the loading="lazy" attribute for thumbnails and the preload config in PhotoSwipe. This reduces LCP and saves bandwidth. For retina displays, add srcset—without it, images lose sharpness. See the srcset example below. PhotoSwipe 5 loads the first image 1.5 times faster than Fancybox 5, and GLightbox is 40% faster for simple galleries.
Integration Examples
Start with installing the package: npm install photoswipe. Import styles: import 'photoswipe/style.css'. Initialize the lightbox with preload and zoom settings. Add data attributes to full-size links. Set up event handlers for closing and focus return.
PhotoSwipe Integration in 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="Photo gallery"> {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> ); } Adaptive Images with 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 Loading with 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> ); } Full Vue 3 Example with 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> Practical Considerations
Timeline and Cost
Basic GLightbox integration with CSS columns—from $500 (0.5 day). PhotoSwipe with masonry, srcset, caption, and custom UI—$1500 (1.5–2 days). Full-featured gallery with API loading, pagination, filters, and video—$3000 (4–5 days). Cost is calculated individually—we'll assess after a brief. Order a comprehensive implementation and get a ready-made solution.
Common Beginner Mistakes
- No lazy loading: all full-size images load at once, LCP suffers.
- Ignoring
srcset: pixelation on retina screens. - No focus return: screen readers get lost after closing.
- Forgetting
preloadfor neighbors: click leads to long load.
Why Choose Us
Over the years, we've implemented lightboxes in dozens of projects of varying complexity—from e-commerce stores to stock photo sites. We use up-to-date libraries, optimize Core Web Vitals, and guarantee quality. Get a consultation—we'll pick the optimal solution for your stack.







