npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cloudinary/product-gallery

v2.0.2

Published

Cloudinary Gallery Widget — an embeddable product media gallery with support for images, videos, and 3D models.

Readme

Cloudinary Gallery Widget — an embeddable product media gallery with support for images, videos, and 3D models.

Built by Cloudinary.

Documentation

Full documentation: cloudinary.com/documentation/product_gallery

Installation

npm install @cloudinary/product-gallery

Usage — React / Next.js

// in layout.tsx
import '@cloudinary/product-gallery/index.css';

// page.tsx (Server Component)
import { resolveConfig } from '@cloudinary/product-gallery/server';
import { GalleryClient } from './GalleryClient';

export default async function Page() {
  const resolvedConfig = await resolveConfig({
    cloudName: 'your-cloud-name',
    mediaAssets: [{ publicId: 'sample' }],
  });

  return <GalleryClient resolvedConfig={resolvedConfig} />;
}
// GalleryClient.tsx (Client Component)
'use client';
import { Gallery } from '@cloudinary/product-gallery';

export function GalleryClient({ resolvedConfig }) {
  return <Gallery resolvedConfig={resolvedConfig} />;
}

Note — the @cloudinary/product-gallery/index.css import above relies on a CSS bundler (as in Next.js). If you are not using one (e.g. manual SSR with Express), you can for example import the stylesheet as a string and inline it instead: import galleryStyles from '@cloudinary/product-gallery/index.css', then render <style dangerouslySetInnerHTML={{ __html: galleryStyles }} /> in your document <head>.

Usage — Script embed (standalone build)

<script src="https://product-gallery-v2.cloudinary.com/latest/all.js"></script>
<div id="my-gallery"></div>
<script>
  const widget = cloudinary.galleryWidget({
    cloudName: 'your-cloud-name',
    container: '#my-gallery',
    mediaAssets: [{ publicId: 'sample' }]
  });
  widget.render();
</script>

The returned instance has the methods render, hydrate, update, focus, hide, destroy and on, plus two properties kept from v1:

  • widget.options – the options object you passed to galleryWidget(), as the same reference. render(newOptions) and update() don't change it.
  • widget.config – the widget's current config. Right after galleryWidget() it is a copy of the defaults (cloudinary.galleryWidgetInfo.DEFAULTS); once a render(), hydrate() or update() call has resolved, it is the full resolved config (defaults, skin and options merged, with viewportBreakpoints and preload taken as given). It stays readable after destroy().
await widget.render();
widget.config.carouselLocation; // resolved value, default or yours

Moving from v1

Swap the script URL to https://product-gallery-v2.cloudinary.com/latest/all.js. Your existing galleryWidget(…) config keeps working, and v2 renders its own DOM, which carries no data-test hooks unless you ask for them.

If your page's CSS or JS targets the widget's internal DOM (v1 data-test hooks such as gallery-viewer-wrap or carousel-nav-next, or v1 class names), also add useLegacyV1Mode: true:

cloudinary.galleryWidget({ cloudName: 'your-cloud-name', container: '#my-gallery', mediaAssets, useLegacyV1Mode: true });

Deprecated – useLegacyV1Mode is a temporary migration aid for script embeds and will be removed in a future major version. Plan to move your page to v2's documented API (and testAttributes for test hooks) instead of the widget's internals. The npm package ignores it.

| Config | Script embed (all.js) | npm package | |---|---|---| | neither set (default) | no hooks | no hooks | | testAttributes: true | v2 data-test names | v2 data-test names | | useLegacyV1Mode: true (with any testAttributes) | v1 data-test names | ignored: follows testAttributes |

In legacy v1 mode the hooks keep v1's names. Only the thumbnail strip differs from v2, because v1 built it from a carousel: carousel (v2 thumbnails-wrapper), carousel-inner-wrapper (thumbnails-inner-wrapper), carousel-nav-prev / carousel-nav-next (thumbnails-nav-prev / thumbnails-nav-next), carousel-item-<n> (thumbnail-item-<n>) and the bare index <n> on the thumbnail button (thumbnail-asset-<n>). Icons also get v1's svg-image. hydrate() ignores the flag, to match the npm package's server markup.

Legacy v1 mode also keeps the v1 structure host CSS selects through, without changing how v2 behaves:

  • v1's elements: gallery-spacer between viewer and navigator (in place of the gap, same size), the thumbnail chain carousel > carousel-wrapper > carousel-inner-wrapper > carousel-item-<n> > slide-item-wrap > thumbnails-wrap (with v1's thumbnails-wrap class; the button gets v1's assetWrapper class), selected-wrap over the selected thumbnail (an empty styling hook: v2 still draws the selected border, your CSS can restyle the hook), and carousel > carousel-wrapper and slide-item-wrap / fade-item-wrap in the viewer.
  • v1's bare attributes: mode on gallery-wrap and gallery-thumbnail-wrap, selected="true" on the selected indicator, shape and direction on the arrows.

Legacy v1 mode also brings back what made v1 pages look the way they did, while sizes and image URLs stay v2's (responsive layout, srcset):

  • Layout: expanded-mode rows take the aspect ratio on their content box, so a host border adds to a row's height as it did on v1's content-sized rows.
  • Specificity: the widget's styles carry v1's one-class specificity (the root gets pgw-v1 instead of pgw), so host rules that overrode v1's styles, such as arrow colours and borders, still apply.

Not restored: v1's generated class names (css-…), img-zoom-wrap, v1's other bare attributes (color, size, radius, …), the zoom pop-up's v1 child order (selectors such as [data-test="popup"] div:nth-child(2) > button don't match v2's pop-up), and native disabled on arrows, which stay aria-disabled and focusable, so button[disabled] / :disabled selectors don't match.

The script build also serves an empty image.js next to all.js, so pages that still preload v1's image.js chunk don't get a 404.

Responsive sizing

The gallery viewer is fully responsive: it fills the width of its parent element and derives its height from the configured aspect ratio using native CSS (width: 100% + aspect-ratio). To size the gallery, constrain its parent container — don't pass pixel dimensions to the widget.

// The gallery will be 800px wide on desktop and shrink with the viewport.
<div style={{ maxWidth: 800, width: '100%', margin: '0 auto' }}>
  <Gallery resolvedConfig={resolvedConfig} />
</div>
<!-- Same idea for the standalone embed -->
<div id="my-gallery" style="max-width: 800px; width: 100%; margin: 0 auto;"></div>

viewerWidthHint (SSR, optional)

viewerWidthHint is an optional config setting. When rendering on the server, the widget cannot measure its container, so the server-generated markup (including responsive srcSet) may not match what the client computes on hydration. If you want server and client markup to line up exactly, set viewerWidthHint to the width your container will occupy at render time — typically the same value as its CSS max-width.

const MAX_WIDTH = 800;

const resolvedConfig = await resolveConfig({
  cloudName: 'your-cloud-name',
  mediaAssets: [{ publicId: 'sample' }],
  viewerWidthHint: MAX_WIDTH,
});

return (
  <div style={{ maxWidth: MAX_WIDTH, width: '100%', margin: '0 auto' }}>
    <GalleryClient resolvedConfig={resolvedConfig} />
  </div>
);

Notes:

  • Only needed for SSR. Omit it in purely client-side usage — the widget will measure the container directly.
  • The hint drives srcSet selection and internal dimension calculations so server and client agree on hydration. It does not force the viewer to render at that pixel width — the CSS still handles visual sizing.
  • Should match the container's effective layout width. A significant mismatch will cause React to log a hydration-dimension warning and may result in a suboptimal srcSet pick.