Image Gallery with Lightbox Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Image Gallery (Lightbox) on Website

Gallery with lightbox is one of the most frequent UI components. Task seems simple until you deal with touch swipes, lazy loading, zoom, neighbor preloading, and accessibility.

Libraries

PhotoSwipe 5 — best option for most tasks. Clean vanilla JS, touch-first, zoom, srcset support. Actively maintained, ~20 KB gzip.

GLightbox — lightweight (~12 KB), supports video and iframe in lightbox, no dependencies.

Fancybox 5 — rich functionality, but paid for commercial projects.

For simple cases without zoom and video — GLightbox. For full photo gallery — PhotoSwipe.

PhotoSwipe: Integration

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'),
      // Neighbor preloading
      preload: [1, 2],
      // Animation
      showHideAnimationType: 'zoom',
      // Close on background click
      closeOnVerticalDrag: true,
      // Zoom
      maxZoomLevel: 4,
      initialZoomLevel: 'fit',
      secondaryZoomLevel: 1.5,
    })

    // Custom 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="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>
  )
}

Responsive Images via srcset

// Generate srcset for lightbox — different sizes for different screens
function buildSrcSet(baseUrl: string, sizes: number[]): string {
  return sizes.map(w => `${baseUrl}?w=${w} ${w}w`).join(', ')
}

// In PhotoSwipe 5 — via 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,
  // For mobile — don't load 2560px
  msrc: img.thumbnail,  // placeholder until full load
}))

Masonry Grid Layout

/* CSS Columns — simplest masonry without 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; }
}

For CSS Masonry support (only Firefox with flag) or exact row alignment — Masonry.js or native CSS grid with grid-template-rows: masonry.

Uniform Grid with Known Dimensions

/* If aspect ratio is known in advance */
.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 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' : ''}`}>
      {/* 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 for Video Gallery

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: 'Promotional video',
    },
    {
      href: '/images/photo1.jpg',
      type: 'image',
      description: 'Photo description',
    },
  ],
  autoplayVideos: false,
  loop: false,
  draggable: true,
  dragToleranceX: 40,
  dragToleranceY: 65,
  swipeToClose: true,
})

Keyboard Navigation and Accessibility

// PhotoSwipe supports keyboard natively
// Additionally: return focus when closing
lightbox.on('close', () => {
  // Return focus to element from which we opened
  const opener = document.querySelector('[data-gallery-opener]') as HTMLElement
  opener?.focus()
})

// ARIA for grid
// role="list" + role="listitem" or just native <ul><li>
<ul class="photo-gallery" role="list" aria-label="Projects gallery">
  <li role="listitem">
    <a href="/large/1.jpg"
       aria-label="Open image: Office Project, 2024"
       data-pswp-width="2400"
       data-pswp-height="1600">
      <img src="/thumb/1.jpg" alt="Office Project, 2024" loading="lazy">
    </a>
  </li>
</ul>

Timeline

GLightbox with basic grid and CSS columns — half day. PhotoSwipe with masonry, srcset, caption, custom UI — 1.5–2 days. Full gallery with API loading, pagination, category filters, video support — 4–5 days.