Media Grid
betaPhotos and videos as a grid of square tiles that open the Lightbox, with your photo consent rule applied.
Preview
As a member
Tab in, then use the arrows. Enter opens the photo. The photo with withdrawn consent is not shown at all.
As a media manager, with actions
People who manage media see the hidden photo as a tile that says why, and names in every consent note.
Hidden until consent is on file
An app that hides photos until consent is on file. Members see only the photos with everyone's consent.
Empty
No photos yet.
- No photos yet
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/media-grid
Use it for
- A set of photos and videos, like an event's gallery or a team's album.
- Name the set with label. It names the list and the lightbox.
- Pass manage only to people who manage your media. They see hidden photos as tiles that say why.
- Set minSize for smaller or larger tiles. Columns fill the width.
- Return false from onOpen to open a photo yourself, like on its own page.
Avoid it for
- One photo. Use MediaImage.
- Files, documents, or a list of attachments.
- Photos that people pick, as in a form. Its tiles open the viewer, they do not select.
Props
- items
Required. The photos and videos, in order. See MediaImage for MediaItem.
MediaItem[]
- manage
For people who manage your media: hidden photos show as tiles that say why, and the lightbox names people.
boolean · default false
- minSize
The smallest a tile gets, in pixels. Tiles sit 6px apart.
number · default 150
- label
The set's name, for the list and the lightbox.
string · default "Photos"
- actions
Icon buttons in the lightbox for the photo shown.
(item: MediaItem) => { icon: ReactNode; label: string; onClick: () => void }[]
- onOpen
A tile was opened, with its place in items. Return false to open it yourself instead of in the lightbox.
(index: number, item: MediaItem) => void | false
- photoConsent
Your consent rule. See MediaImage.
{ who?: "everyone" | "juniors" | "nobody"; missing?: "blur" | "hide" } · default { who: "everyone", missing: "blur" }
- className
Extra classes on the list. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| items | MediaItem[] | - | Required. The photos and videos, in order. See MediaImage for MediaItem. |
| manage | boolean | false | For people who manage your media: hidden photos show as tiles that say why, and the lightbox names people. |
| minSize | number | 150 | The smallest a tile gets, in pixels. Tiles sit 6px apart. |
| label | string | "Photos" | The set's name, for the list and the lightbox. |
| actions | (item: MediaItem) => { icon: ReactNode; label: string; onClick: () => void }[] | - | Icon buttons in the lightbox for the photo shown. |
| onOpen | (index: number, item: MediaItem) => void | false | - | A tile was opened, with its place in items. Return false to open it yourself instead of in the lightbox. |
| photoConsent | { who?: "everyone" | "juniors" | "nobody"; missing?: "blur" | "hide" } | { who: "everyone", missing: "blur" } | Your consent rule. See MediaImage. |
| className | string | - | Extra classes on the list. Later classes win. |
Accessibility
- A list named by label. Each tile is a button named by its photo, plus video and its length, a blur, or why it is hidden.
- One Tab stop. ← and → move along a row, ↑ and ↓ move between rows by the grid's own columns, and Home and End go to the ends. Right to left, ← and → swap.
- Enter or Space opens the photo in the Lightbox. When it closes, focus is on the tile of the photo you were looking at.
- Members never meet a hidden photo, not even in the Tab order.
Examples
A gallery as members see it
<MediaGrid items={photos} label="Photos from the team offsite" />For a media manager, with actions
import { Download, Trash2 } from "lucide-react";
<MediaGrid
items={photos}
label="Photos from the launch"
manage
minSize={120}
actions={(photo) => [
{ icon: <Download />, label: "Download", onClick: () => download(photo) },
{ icon: <Trash2 />, label: "Delete", onClick: () => remove(photo) },
]}
/>