Skip to content

Repository files navigation

sanity-plugin-sgntech-gallery — image galleries as a Portable Text block

sanity-plugin-sgntech-gallery

Image galleries as a Portable Text block for Sanity Studio — grid or carousel, fed from the Sanity Media Library, with a design-agnostic React renderer.

npm version license


What you get

  • A gallery block for Portable Text — several images as one block, dropped wherever they belong in the article.
  • Images come from the Sanity Media Library. The block uses regular image fields, so every asset source your Studio has is available. Set disableNew: true to hide the upload UI and force editors to pick from the library.
  • Alternative text, caption and credit per image — the caption is rendered underneath each slide, with the credit next to it.
  • Two layouts: grid (1–6 columns) and carousel — one image at a time, stepped through with arrows and dot indicators underneath.
  • Lightbox: clicking an image opens it full screen, with arrows, a counter, keyboard navigation and Escape to close. Built on the native <dialog>, so focus handling comes from the browser.
  • sanity-plugin-sgntech-gallery/render — a React renderer and a @portabletext/react mapping. That entry point does not import sanity, so your app bundle stays clean.
  • Design-agnostic markup. The stylesheet only handles structure — no colours, no fonts, no shadows. Everything inherits from your page, and every value worth changing is a CSS custom property. The block looks like part of your design without a single override.

Installation

npm install sanity-plugin-sgntech-gallery

Requires Sanity Studio v5 or v6 and React 18 or 19. No runtime dependencies.

📦 On npm: sanity-plugin-sgntech-gallery

Usage in the Studio

// sanity.config.ts
import {defineArrayMember, defineConfig, defineField} from 'sanity'
import {gallery} from 'sanity-plugin-sgntech-gallery'

export default defineConfig({
  // ...
  plugins: [gallery()],

  schema: {
    types: [
      {
        type: 'document',
        name: 'article',
        fields: [
          defineField({
            type: 'array',
            name: 'body',
            of: [
              defineArrayMember({type: 'block'}),
              defineArrayMember({type: 'gallery'}), // ← the gallery block
            ],
          }),
        ],
      },
    ],
  },
})

Editors pick Gallery from the "Add item" menu, add images, and fill in alternative text, caption and credit per image.

Options

gallery({
  name: 'gallery', // schema type name
  title: 'Gallery', // label in the editor's insert menu
  disableNew: false, // true = no uploads, Media Library only
  mediaLibraryFilters: [], // GROQ filters offered in the asset dialog
  assetSources: undefined, // restrict which asset sources this field offers
  hotspot: true, // hotspot/crop editor
  requireAlt: true, // make alternative text mandatory
  layouts: ['grid', 'carousel'], // layouts editors may choose from
  defaultLayout: 'grid',
  defaultColumns: 3,
  maxImages: undefined, // upper limit for the number of images
})

Media Library

The block stores plain Sanity image references, so it works with whatever asset sources your Studio offers. To use Sanity Media Library assets, enable it in your Studio config:

export default defineConfig({
  // ...
  mediaLibrary: {enabled: true},
  auth: {loginMethod: 'token'}, // recommended by Sanity for private assets
})

Force editors to pick from the library instead of uploading ad hoc:

gallery({disableNew: true})

Offer filters inside the asset dialog — each is a GROQ filter that runs against the library:

gallery({
  mediaLibraryFilters: [
    {name: 'Press photos', query: 'defined(aspects.pressPhoto)'},
    {name: 'Live shots', query: 'aspects.category == "live"'},
  ],
})

If an asset carries alternative text in the library, the renderer falls back to it when the editor left the alt field empty.

Stored value

{
  "_type": "gallery",
  "_key": "a1b2c3",
  "layout": "carousel",
  "columns": 3,
  "caption": "Waldbühne 2026",
  "images": [
    {
      "_key": "d4e5f6",
      "_type": "galleryImage",
      "asset": {"_type": "reference", "_ref": "image-abc123-2000x1333-jpg"},
      "alt": "Crowd at the barrier",
      "caption": "Berlin, Waldbühne",
      "credit": "Photo: E. Hohmann"
    }
  ]
}

Rendering in your frontend

Fetch the gallery with the asset dereferenced — the renderer uses url, the dimensions and the library's altText when they are there:

body[]{
  ...,
  _type == "gallery" => {
    ...,
    images[]{
      ...,
      asset->{url, altText, metadata{lqip, dimensions}}
    }
  }
}

With @portabletext/react

import {PortableText} from '@portabletext/react'
import {galleryPortableTextComponents} from 'sanity-plugin-sgntech-gallery/render'

;<PortableText value={article.body} components={galleryPortableTextComponents} />

Merge it with your own block types:

const components = {
  types: {
    ...galleryPortableTextComponents.types,
    image: MyImageBlock,
  },
}

Standalone component

import {Gallery} from 'sanity-plugin-sgntech-gallery/render'

;<Gallery value={gallery} className="my-gallery" />

With @sanity/image-url or next/image

import imageUrlBuilder from '@sanity/image-url'
import Image from 'next/image'
import {Gallery} from 'sanity-plugin-sgntech-gallery/render'

const builder = imageUrlBuilder(client)

;<Gallery
  value={gallery}
  resolveImageUrl={(image) => builder.image(image).width(1200).auto('format').url()}
  renderImage={(image, {src, alt}) => (
    <Image className="sgn-gallery__image" src={src!} alt={alt} width={1200} height={800} />
  )}
/>

Props

Prop Default Purpose
value The gallery value from Sanity
layout / columns from the value Override what the editor picked
showCaptions true Render captions underneath each image
controls true Previous/next buttons in the carousel
indicators true Dot indicators underneath the carousel
lightbox true Open images full screen on click
loop false Wrap around at the first and last image
resolveImageUrl Build image URLs yourself
resolveLightboxUrl resolveImageUrl Larger URL for the lightbox
renderImage Replace the <img> element
sizes, loading lazy Passed through to the image
injectStyles true Add the structural stylesheet to the document
className, label Extra class, accessible name
labels English Button texts — previous, next, close, goTo, open

Styling

The renderer ships structural CSS only — grid, scroll-snap, aspect ratio. Colours, fonts and spacing come from your page. Every selector is a single class, so your own rules win without !important.

.sgn-gallery {
  --sgn-gallery-gap: 1.5rem;
  --sgn-gallery-columns: 3; /* set by the block, override if you like */
  --sgn-gallery-radius: 12px;
  --sgn-gallery-aspect: 4 / 3; /* auto = keep the original ratio */
  --sgn-gallery-object-fit: cover;
  --sgn-gallery-caption-gap: 0.5rem;
  --sgn-gallery-caption-size: 0.875em;
  --sgn-gallery-caption-opacity: 0.75;
  --sgn-gallery-slide-width: 100%; /* e.g. min(100%, 34rem) to peek at the next slide */
  --sgn-gallery-control-size: 2.25rem;
  --sgn-gallery-control-radius: 999px;
  --sgn-gallery-dot-size: 0.5rem;
  --sgn-gallery-dot-gap: 0.5rem;
  --sgn-gallery-lightbox-backdrop: rgba(0, 0, 0, 0.82);
  --sgn-gallery-lightbox-bg: transparent;
  --sgn-gallery-lightbox-color: #fff;
  --sgn-gallery-lightbox-padding: 2rem;
}

Class names: sgn-gallery, __list (--grid / --carousel), __item, __figure, __image, __trigger, __caption, __credit, __footer, __controls, __control, __dots, __dot, __lightbox, __lightbox-inner, __lightbox-image, __lightbox-caption, __lightbox-nav, __lightbox-counter, __lightbox-close.

The active dot carries aria-current="true", so styling the current slide indicator needs no extra class.

Prefer to own the CSS completely? Pass injectStyles={false} and import galleryStyles as a starting point, or write your own from scratch.

Accessibility

  • Alternative text is required by default (requireAlt). An empty alt marks an image as decorative, which is the correct choice for purely ornamental pictures — the plugin never falls back to the caption for alt, since that would make screen readers repeat the visible text.
  • The carousel is a keyboard-focusable scroll region, so it can be scrolled without a mouse. Arrows and dots are real buttons with labels; the dots expose aria-current.
  • The lightbox is a native modal <dialog>: the browser traps focus, closes it on Escape and returns focus to the image that opened it. Left/right arrow keys step through the images, and a click on the backdrop closes it.
  • prefers-reduced-motion disables the smooth scrolling.
  • Captions use figure/figcaption, so the association is explicit.

Develop

npm install
npm test            # unit tests for the value helpers
npm run lint
npm run build
npm run link-watch  # build + publish to a local yalc repo for Studio testing

Built with @sanity/plugin-kit.

License

MIT © SGNTech

About

Image galleries as a Portable Text block for Sanity Studio — grid or carousel, fed from the Sanity Media Library, with a design-agnostic React renderer.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages