Behive Tech Component Registry

Gallery

v0.1.4Draft

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`.

pnpm add @behivetech/organisms.gallery

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

Open in new tab

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. columns is 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 in images[] 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. Pass next/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 through labels.
  • Empty. An empty images array 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.

Properties

Gallery props

PropTypeDefaultDescription
images*GalleryImage[]—The images, in the order the grid, Tab and the lightbox follow. An empty array renders nothing
classNamestring—Additional class names to merge with the component root element
columnsGalleryColumns{ 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 }`.
labelsPartial<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
renderImageGalleryRenderImage(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
ParameterTypeDefaultDescription
resetKeystring——

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; }; }
ParameterTypeDefaultDescription
__0UseSwipeOptions——

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
ParameterTypeDefaultDescription
rootRefRefObject<HTMLElement | null>——
canGoPreviousboolean——
canGoNextboolean——

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; }
ParameterTypeDefaultDescription
__0UseLightboxOptions——

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>
ParameterTypeDefaultDescription
columnsGalleryColumns——

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
ParameterTypeDefaultDescription
__0GalleryImage——

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
ParameterTypeDefaultDescription
columnsRecord<GalleryBreakpoint, number>——

Returns string