Gallery Block
Image galleries as a Portable Text block: grid or carousel, fed from the Sanity Media Library. Every image carries alternative text, caption and credit, and the caption is rendered underneath each slide. The carousel steps through one image at a time with arrows and dot indicators; clicking any image opens it full screen in a native <dialog> lightbox with keyboard navigation. The renderer ships structural CSS only — colours, fonts and spacing come from your design via CSS custom properties.
Install command
npm i sanity-plugin-sgntech-gallery
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.
What you get
- A
galleryblock 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
imagefields, so every asset source your Studio has is available. SetdisableNew: trueto 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/reactmapping. That entry point does not importsanity, 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-galleryRequires 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 emptyaltmarks an image as decorative, which is the correct choice for purely ornamental pictures — the plugin never falls back to the caption foralt, 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-motiondisables 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 testingBuilt with @sanity/plugin-kit.
License
MIT © SGNTech
