40 / Blocks
Image Gallery
A responsive image collection with equal and masonry layouts, plus a lightbox that handles focus, keyboard navigation, and scroll locking.
From the archive
6 images"use client"
import { ImageGallery } from "mischief-ui/image-gallery"
export const galleryImages = [
{
id: "shift-button",
src: "/demo/gallery/shift-button.png",
alt: "Shift Button experiment",
width: 423,
height: 345,
caption: "Shift Button",
description: "A call to action that makes room for what comes next.",
},
{
id: "floating-deck",
src: "/demo/gallery/floating-deck.png",
alt: "Floating Deck experiment",
width: 658,
height: 439,
caption: "Floating Deck",
description: "A compact home for apps, components, and notes.",
},
{
id: "impossible-checkbox",
src: "/demo/gallery/impossible-checkbox.png",
alt: "Impossible Checkbox experiment",
width: 390,
height: 417,
caption: "Impossible Checkbox",
description: "The bear still refuses to cooperate.",
},
{
id: "floating-index",
src: "/demo/gallery/floating-index.png",
alt: "Floating Index experiment",
width: 487,
height: 342,
caption: "Floating Index",
description: "A small outline that keeps your place.",
},
{
id: "appearance-control",
src: "/demo/gallery/appearance-control.png",
alt: "Appearance Control experiment",
width: 581,
height: 459,
caption: "Appearance Control",
description: "Theme choices gathered into one useful card.",
},
{
id: "focus-text",
src: "/demo/gallery/focus-text.png",
alt: "Focus Text experiment",
width: 900,
height: 571,
caption: "Focus Text",
description: "A sentence that sharpens as you reach it.",
},
]
export function ImageGalleryDemo() {
return <ImageGallery images={galleryImages} title="From the archive" />
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/image-galleryimport { ImageGallery } from "mischief-ui/image-gallery"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Grid2X2, Images, Rows3 } from "lucide-react" import { cn } from "@/lib/utils"import { ImageGrid, type GalleryImage, type ImageGridLayout,} from "@/registry/default/image-grid/image-grid"import { Lightbox } from "@/registry/default/lightbox/lightbox" export type ImageGalleryItem = GalleryImageUsage
const images = [
{
id: "studio",
src: "/photos/studio.jpg",
alt: "Sunlight across the studio table",
width: 1600,
height: 1200,
caption: "The studio",
},
]
export function WorkGallery() {
return <ImageGallery images={images} title="Recent work" />
}Grid or masonry
The grid gives every image the same cell, which is the right choice when the pictures are alike and comparison matters. Masonry uses CSS columns and lets each image keep its own height, which suits a mixed set where cropping would be a loss.
Masonry fills one column top to bottom before starting the next, so the visual order runs down rather than across. Where sequence carries meaning -- pages of a document, steps in order -- use the grid, because the reading order people expect and the order they are laid out in will not match.
Dimensions and loading
Give width and height wherever you know them. They reserve the right space before the image arrives, so the gallery does not reflow underneath the reader as pictures load -- and in masonry, so the columns do not rebalance twice.
Everything below the fold should stay lazy. Set loading to "eager" only for the first row or two, which are the ones the reader is waiting on.
Base UI supplies the lightbox dialog, with its focus trap, scroll lock, Escape handling, and focus restoration, so this component is imported from its own entry and needs @base-ui/react installed.
API
imagesImageGalleryItem[]Image sources, alt text, captions, and optional downloads.titleReactNodeThe heading above the collection.layout"grid" | "masonry"The layout when controlled.defaultLayout"grid" | "masonry"The initial uncontrolled layout.onLayoutChange(layout) => voidRuns when the layout changes.selectedIdstring | nullThe open image when controlled.onSelectedIdChange(id) => voidRuns when the lightbox opens, moves, or closes.showLayoutTogglebooleanShows or hides the layout control.emptyStateReactNodeContent shown when the collection is empty.renderImage(image, context) => ReactNodeUses a framework image component or another custom renderer.Accessibility
Every thumbnail is a named button. Base UI supplies the modal dialog, focus trap, scroll lock, Escape handling, and focus restoration. Left and Right Arrow move between images. Captions, position, and close controls remain available without hover.