Gallery
v0.1.4DraftA responsive masonry image grid that opens a swipeable, keyboard-navigable lightbox. Tiles keep each image's ratio, captions show in the lightbox only, and the lightbox is a fullscreen `Modal`.
A responsive masonry image grid that opens a swipeable, keyboard-navigable lightbox. Tiles keep each image's ratio, captions show in the lightbox only, and the lightbox is a fullscreen Modal.
Usage
The jsx block above runs live on the docs site. Imports are stripped and every registry export is in scope, so you can compose with other components too.
Behaviour
- Columns.
columnsis a number or{ xs, md, lg }(below 48rem, from 48rem, from 64rem). The default is{ xs: 2, md: 3, lg: 4 }. It is one list inimages[]order, so Tab, reading and lightbox order agree; the grid drops each tile into the column that frees up first. All of it is CSS, so there is no reflow after hydration, and up to 8 columns are supported. - Tall images. A tile keeps its image's ratio up to about 3:1 (height over width). The row span is capped at 150 rows, so a taller image is cropped to that height in its thumbnail (
object-fit: cover); the lightbox still shows the whole image. Crop very tall images before you pass them in if the thumbnail should show a particular part. - Lightbox. Previous and Next are disabled on the first and last image; it does not wrap. ArrowLeft and ArrowRight step, Home and End jump, Escape closes. On close, focus returns to the thumbnail of the image showing at that moment.
- Swipe. A horizontal swipe past 50px, or a flick faster than 0.3px/ms, steps to the next or previous image. It uses pointer events and no dependency.
renderImage. Passnext/image(or any<img>):renderImage={(image, { sizes }) => <Image src={image.src} alt={image.alt} width={image.width} height={image.height} sizes={sizes} />}. The default is a plain lazy<img>.- Text. The built-in English texts (
Close,Previous image, ...) are replaceable throughlabels. - Empty. An empty
imagesarray renders nothing; the page owns the empty state.
Styles: install the stylesheets of the packages it composes along with this one (@behivetech/organisms.gallery/styles.css, plus atoms.modal, atoms.button, atoms.icon and atoms.spinner). It reads the design tokens from @behivetech/cms.base-styles.
Examples
Open all 3 →Properties
Gallery props
| Prop | Type | Default | Description |
|---|---|---|---|
images* | GalleryImage[] | — | The images, in the order the grid, Tab and the lightbox follow. An empty array renders nothing |
className | string | — | Additional class names to merge with the component root element |
columns | GalleryColumns | {
xs: 2,
md: 3,
lg: 4,
} | Columns: a number for every width, or `{ xs, md, lg }` for below 48rem, from 48rem and from 64rem (any you omit keep the default). Up to 8. Defaults to `{ xs: 2, md: 3, lg: 4 }`. |
labels | Partial<GalleryLabels> | — | Replaces any of the built-in English texts, for translation |
onOpenChange | ((open: boolean, index: number) => void) | — | Called with `true` and the opened index when the lightbox opens, and with `false` and the index showing when it closes |
renderImage | GalleryRenderImage | (image, { context }) => (
<img
src={image.src}
alt={image.alt}
width={image.width}
height={image.height}
loading={context === "thumbnail" ? "lazy" : "eager"}
decoding="async"
/>
) | Draws each image, so an app can pass `next/image`. Receives the image and `{ context: "thumbnail" | "lightbox", sizes }`. Defaults to a lazy `<img>`. Render a plain `<img>` (or a component that renders one): the grid fills the tile with it. |
Also accepts the native attributes of its root element (className, aria-*, event handlers, …).
Functions
useImageStatus hook
Tracks whether the `<img>` inside a wrapper has loaded or failed, whatever component drew it (the default `<img>` or an app's `next/image`). `resetKey` names the image: a new key starts again at `loading`. An image that finished before hydration fires no event, so a new key also reads `complete`.
useImageStatus(resetKey: string): ImageStatusResult| Parameter | Type | Default | Description |
|---|---|---|---|
resetKey | string | — | — |
Returns ImageStatusResult
useSwipe hook
Touch swipe with pointer events and no dependency. The image follows the finger by writing one custom property on the tracked element (re-rendering on every move would be wasteful); the stylesheet turns it into a transform. A swipe left goes next, right goes previous; at either end the drag rubber-bands and snaps back.
useSwipe(__0: UseSwipeOptions): { trackRef: RefObject<HTMLDivElement | null>; handlers: { onPointerDown: (event: SwipeEvent) => void; onPointerMove: (event: SwipeEvent) => void; onPointerUp: (event: SwipeEvent) => void; onPointerCancel: (event: SwipeEvent) => void; }; }| Parameter | Type | Default | Description |
|---|---|---|---|
__0 | UseSwipeOptions | — | — |
Returns { trackRef: RefObject<HTMLDivElement | null>; handlers: { onPointerDown: (event: SwipeEvent) => void; onPointerMove: (event: SwipeEvent) => void; onPointerUp: (event: SwipeEvent) => void; onPointerCancel: (event: SwipeEvent) => void; }; }
useStrandedFocus hook
A disabled button cannot keep focus, so when Previous or Next becomes disabled while holding it (or focus fell to the page), hand it to the other arrow. `rootRef` holds exactly those two buttons, in that order; Close sits outside it.
useStrandedFocus(rootRef: RefObject<HTMLElement | null>, canGoPrevious: boolean, canGoNext: boolean): void| Parameter | Type | Default | Description |
|---|---|---|---|
rootRef | RefObject<HTMLElement | null> | — | — |
canGoPrevious | boolean | — | — |
canGoNext | boolean | — | — |
Returns void
useLightbox hook
Open state for the gallery lightbox, and where focus goes when it closes: the thumbnail of the image showing at that moment, not the one first opened, scrolled into view.
useLightbox(__0: UseLightboxOptions): { openIndex: number | null; open: (index: number) => void; changeIndex: (index: number) => void; close: () => void; restoreFocus: (event: Event) => void; registerThumbnail: (index: number) => (element: HTMLButtonElement | null) => void; }| Parameter | Type | Default | Description |
|---|---|---|---|
__0 | UseLightboxOptions | — | — |
Returns { openIndex: number | null; open: (index: number) => void; changeIndex: (index: number) => void; close: () => void; restoreFocus: (event: Event) => void; registerThumbnail: (index: number) => (element: HTMLButtonElement | null) => void; }
resolveColumns function
Resolves the `columns` prop to a whole count per breakpoint. A number applies everywhere; an object overrides the defaults per breakpoint.
resolveColumns(columns: GalleryColumns): Record<GalleryBreakpoint, number>| Parameter | Type | Default | Description |
|---|---|---|---|
columns | GalleryColumns | — | — |
Returns Record<GalleryBreakpoint, number>
rowSpan function
How many grid rows a tile spans, from its image's height over width. Tiles of different ratios then stack into columns with the grid's own auto-placement, while the DOM stays in `images[]` order.
rowSpan(__0: GalleryImage): number| Parameter | Type | Default | Description |
|---|---|---|---|
__0 | GalleryImage | — | — |
Returns number
thumbnailSizes function
The `sizes` string for a thumbnail, from the columns it will sit in (the gutter is ignored, so it errs a little large).
thumbnailSizes(columns: Record<GalleryBreakpoint, number>): string| Parameter | Type | Default | Description |
|---|---|---|---|
columns | Record<GalleryBreakpoint, number> | — | — |
Returns string