Skip to content
txbrendan/ui

Media Grid

beta

Photos 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. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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) },
  ]}
/>