Skip to main content

Image gallery

ImagePreview (lightbox), ImageCarousel (in-page), and MasonryGrid with lazy loading.

Quick start
import { ImagePreview, ImageCarousel, MasonryGrid } from "mates-ui";
import type { ImageGalleryItem } from "mates-ui";





const images: ImageGalleryItem[] = [
  { id: "1", src: "…/full.jpg", thumbSrc: "…/thumb.jpg", alt: "Description" },
];

ImagePreview — lightbox

import { html, atom, isSSR, renderApp } from 'mates';
import { ImagePreview, FilledButton } from 'mates-ui';

const IMAGES = [
{ id: "1", src: "https://picsum.photos/id/1015/1600/1067", thumbSrc: "https://picsum.photos/id/1015/200/134", alt: "River canyon" },
{ id: "2", src: "https://picsum.photos/id/1018/1600/900", thumbSrc: "https://picsum.photos/id/1018/200/113", alt: "Hills and fog" },
{ id: "3", src: "https://picsum.photos/id/1025/1600/1200", thumbSrc: "https://picsum.photos/id/1025/200/150", alt: "Puppy" },
{ id: "4", src: "https://picsum.photos/id/1036/1600/900", thumbSrc: "https://picsum.photos/id/1036/200/113", alt: "Railway" },
{ id: "5", src: "https://picsum.photos/id/1043/1600/1000", thumbSrc: "https://picsum.photos/id/1043/200/125", alt: "Forest road" },
];

const App = () => {
const open = atom(false);
const index = atom(0);
return () => html`
<x-col gap="var(--md-space-4)">
${FilledButton("Open image preview", { onClick: () => open.set(true) })}
${ImagePreview({ open: open(), images: IMAGES, index: index(), onIndexChange: (i) => index.set(i), onClose: () => open.set(false) })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

MasonryGrid

import { html, atom, isSSR, renderApp } from 'mates';
import { MasonryGrid } from 'mates-ui';

function makeItems(start, count) {
return Array.from({ length: count }, (_, i) => {
const n = start + i;
const w = 600 + ((n * 37) % 200); const h = 400 + ((n * 53) % 280);
return { id: "m-" + n, src: "https://picsum.photos/seed/md" + n + "/" + w + "/" + h, thumbSrc: "https://picsum.photos/seed/md" + n + "/120/120", alt: "Sample " + n };
});
}

const App = () => {
const items = atom(makeItems(0, 12));
const hasMore = atom(true);
let page = 1;
const loadMore = () => {
if (!hasMore()) return;
items.set([...items(), ...makeItems(page * 12, 8)]);
page += 1;
if (page >= 4) hasMore.set(false);
};
return () => html`
${MasonryGrid({ items: items(), columns: 3, hasMore: !isSSR() && hasMore(), onLoadMore: loadMore })}
`;
};

renderApp(App, document.getElementById('app'));

ImagePreview / ImageCarousel / MasonryGrid

import { ImagePreview, ImageCarousel, MasonryGrid } from "mates-ui"; import type { ImageGalleryItem } from "mates-ui";
Prop Type Default Description
alt string Accessible alt text for the image.
id* string Unique identifier for the gallery item.
src* string Full-resolution image URL.
thumbSrc string Thumbnail URL used in strip and grid views.

ImagePreview

Prop Type Default Description
closeGalleryLabel string "Close gallery" Close button aria-label.
images* ImageGalleryItem[] Array of gallery items to display in the lightbox.
index* number Index of the currently visible image.
onClose* () => void Called when the user closes the lightbox (Esc or close button).
onIndexChange* (index: number) => void Called when the user navigates to a different image.
open* boolean Controls whether the lightbox is visible.

ImageCarousel

Prop Type Default Description
autoFocus boolean true Whether the carousel receives focus on mount.
carouselLabel string "Image carousel" Carousel region aria-label.
images* ImageGalleryItem[] Array of gallery items to display in the carousel.
index* number Index of the currently visible slide.
onIndexChange* (index: number) => void Called when the slide changes (arrows, keyboard, swipe).

MasonryGrid

Prop Type Default Description
columns number 3 Number of CSS columns. Adjusts to container width at smaller breakpoints.
hasMore boolean false When true, an IntersectionObserver sentinel triggers onLoadMore.
items* ImageGalleryItem[] Array of gallery items to lay out in the masonry grid.
onLoadMore () => void Called when the sentinel scrolls into view and hasMore is true.

Caveats

  • All three components share the `ImageGalleryItem` type (`id`, `src`, `thumbSrc`, `alt`).
  • ImagePreview requires a container with position:relative or a portal target.
  • MasonryGrid uses CSS columns internally — column count adjusts to container width.