PhotoSwipe Gallery with Captions — Free JS Snippet

PhotoSwipe Full-Screen Gallery with Captions · Media · Plain HTML, CSS & JS · Live preview

What's included

Features

Touch gestures: swipe, pinch, double-tap zoom and drag-down-to-close
Keyboard arrows, Escape and mouse-wheel zoom on desktop
Zoom-from-thumbnail open and close animation
Custom caption bar registered on PhotoSwipe's own UI layer
Lightbox and core module split so the viewer loads only when needed
Responsive mosaic grid with a featured first image
Explicit pixel sizes via data-pswp-width and data-pswp-height
Focus-visible outlines and descriptive aria-labels on every thumbnail

About this UI Snippet

PhotoSwipe Full-Screen Gallery with Captions — HTML, CSS & JavaScript

Screenshot of the PhotoSwipe Full-Screen Gallery with Captions snippet rendered live

A lightbox has three jobs: show the picture large, let people move between pictures without thinking, and get out of the way on a phone. PhotoSwipe is built around the mobile case. It handles swipe between slides, pinch-to-zoom, double-tap to zoom, vertical drag to close and pan when zoomed, using the same gestures people already know from their phone's photo app, and it also supports keyboard arrows, Escape, the mouse wheel and browser history.

Version 5 splits the library in two: a small lightbox module that lives on the page and finds your thumbnails, and the core viewer, loaded only when someone actually opens a photo. Both are passed to the constructor — gallery is a selector for the container, children finds the links inside it, and pswpModule supplies the viewer. In this snippet both UMD scripts are loaded from a CDN so the globals PhotoSwipeLightbox and PhotoSwipe exist; in a bundler setup the core is usually a dynamic import so it stays out of the initial page weight.

The requirement people trip over is size. PhotoSwipe needs each image's real pixel dimensions before it opens, supplied as data-pswp-width and data-pswp-height on the link, because it uses them to lay out the slide and compute the thumbnail-to-fullscreen zoom animation. Without them the viewer cannot animate, or the image renders at the wrong size. showHideAnimationType: 'zoom' is the effect where the thumbnail visibly grows into the viewer, and it depends on that data being right.

Captions are not built in; they are added by registering a custom UI element. On the uiRegister event, pswp.ui.registerElement creates a caption container appended to the viewer's root, and its onInit hook listens for the change event to update the text from the current slide's source link. Because it belongs to PhotoSwipe's UI layer, the caption fades with the other controls when the user is idle. The gradient background keeps white text readable on both light and dark photos. The images here are generated on a canvas so the demo is self-contained; in a real gallery they are ordinary image URLs.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add a download button in the viewer toolbar, share links using registerElement, or lazy-load the PhotoSwipe core with a dynamic import.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a thumbnail gallery with PhotoSwipe 5 loaded from a CDN (photoswipe.css plus the UMD lightbox and core scripts).

Requirements:
- Render eight links with data-pswp-width and data-pswp-height, each containing an <img> thumbnail; the first spans two grid cells.
- Create new PhotoSwipeLightbox({ gallery, children: 'a', pswpModule: PhotoSwipe, showHideAnimationType: 'zoom', wheelToZoom: true }) and call init().
- Add a caption bar by registering a custom UI element on the 'uiRegister' event, updating it on the 'change' event with the current slide title, description and "n of total".
- Give every thumbnail an aria-label and a visible focus outline.
- Generate the images on canvases so the demo needs no files.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="ps-wrap">
  <h3 class="ps-title">Iceland road trip</h3>
  <p class="ps-sub">Tap a photo. Swipe, pinch or use the arrow keys inside the viewer.</p>
  <div class="ps-grid" id="psGallery"></div>
</div>

Step by step

How to Use

  1. 1
    Open a photoClick any thumbnail. It zooms smoothly into a full-screen viewer with a caption at the bottom.
  2. 2
    Move between photosSwipe, use the arrow keys, or click the arrow buttons. The caption and counter update.
  3. 3
    Zoom inDouble-click or pinch to zoom, use the mouse wheel on desktop, then drag to pan the image.
  4. 4
    Close the viewerPress Escape, click the × button, or drag the image vertically on a touch screen.
  5. 5
    Check the gridHover a thumbnail to see its caption preview; the first image spans two rows and columns.

Real-world uses

Common Use Cases

Photography portfolios and travel blogs
Give visitors a native-feeling photo viewer. For mixed image and video slides see the GLightbox mixed media lightbox.
SHOP
Product image galleries
Let shoppers zoom into fabric and detail shots on a phone without leaving the page.
Event and wedding albums
Share large albums with captions and a clean, fast full-screen experience.
Learning UI-layer extension
A neat example of extending a library through its registerElement API instead of patching its DOM.

Got questions?

Frequently Asked Questions

It needs the real pixel dimensions to lay out the slide and compute the zoom animation from the thumbnail before the full image has loaded.

Register a custom element with pswp.ui.registerElement on the uiRegister event, and update its content on the change event from the current slide's source element.

The lightbox is a small helper that finds your thumbnails and opens the viewer; pswpModule is the larger core viewer, which can be loaded lazily.

Yes. Import PhotoSwipeLightbox and pass pswpModule: () => import("photoswipe") so the viewer is code-split.

Yes. Provide a srcset or use the dataSource option to give different image URLs per screen size.

It supports keyboard navigation and returns focus on close, but you should still provide meaningful alt text and aria-labels on the thumbnail links.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page to convert the markup and styles. The behaviour comes from PhotoSwipe, so in a framework project install it with npm install photoswipe instead of the CDN tag, create the lightbox in useEffect / onMounted / ngAfterViewInit and import the core module lazily, and release it with lightbox.destroy() when the component unmounts.