A three-column masonry of photographs in mixed aspects, each rising into view and drifting inside its frame as the page scrolls, with a serif subject and a small-caps place and month under every plate; any plate opens a paper-toned lightbox that sets the subject large beside the picture, turns pages by clicking either half of the plate, the arrows or the keyboard, and slides each plate in from the side it came from.
A light masonry gallery of mixed crops, each plate drifting inside its frame, opening into a paper-coloured lightbox where the subject is set large beside the picture and either half of it turns the page.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.
Radix flavor
npx shadcn add "https://ui.beste.co/r/gallery28?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/gallery28?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/gallery28.tsx plus the button22 sliding-marker pill and the text1 word stagger it uses.
The installed file exports gallery28Demo alongside the block: the exact props behind the preview above. Spread it to get a working gallery in one line.
import { Gallery28, gallery28Demo } from "@/components/beste/block/gallery28";
export default function Page() {
return <Gallery28 {...gallery28Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Gallery28 } from "@/components/beste/block/gallery28";
export default function Page() {
return (
<Gallery28
eyebrow="Around the rooms"
heading="What the light does here, month by month."
button={{ label: "See the rooms in person", href: "/visit", tone: "outline" }}
plates={[
{
src: "/rooms/front.jpg",
alt: "A sunlit room with plaster walls",
aspect: "portrait",
subject: "Front room",
where: "Rua da Prata, January",
},
]}
labels={{ previous: "Previous plate", next: "Next plate", close: "Close", open: "Open plate", plate: "Plate", of: "of" }}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | – | Small-caps line above the heading |
heading | string | – | Section heading, rendered as an h2 through Text1 |
button | ActionButton | – | Pill action beside the heading |
plates | Plate[] | [] | The photographs, each with its own crop |
labels | Gallery28Labels | {} | Control names and the counter wording |
className | string | – | Extra classes for the outer <section> |
type Plate = {
src: string;
alt: string;
aspect: "portrait" | "landscape" | "square";
subject: string;
where: string;
};
type Gallery28Labels = {
previous?: string;
next?: string;
close?: string;
open?: string;
plate?: string;
of?: string;
};
type ActionButton = {
label: string;
href: string;
tone?: "primary" | "light" | "dark" | "outline";
};columns with break-inside-avoid on each figure, not a grid, so plates of three different crops pack without leaving gaps and without any height being measured in JavaScript.aspect is a fixed union rather than a free string, and it maps to one of three Tailwind classes; that is what lets the mixed crops stay on the design's ratios rather than following whatever the source file happens to be.figure with a real figcaption, so the subject and place stay attached to the picture rather than being two spans that follow it.cursor-w-resize or cursor-e-resize cursor, so the whole picture is a page-turn target and the cursor says which way.role="dialog" with aria-modal, Escape to close, arrow keys to step, and document.body.style.overflow locked while open and restored on unmount.stopPropagation, so using the controls never dismisses the whole thing by accident.bg-background/95 with a backdrop blur, so the page behind stays faintly visible and the plate reads as a page rather than a projection.