Skip to content
txbrendan/ui

Media Image

beta

A photo, or a video's still, with your photo consent rule applied: faces blurred, or the photo hidden, until consent is on file.

Preview

consent

All on file
Face blurred
One face blurred
Faces blurred
Two faces blurred
Blurred
No face tagged
Blurred
A video blurs whole
Consent withdrawn
Consent withdrawn

photoConsent

Face blurred
Juniors only: Ella is blurred
Juniors only: adults show
Waiting for consent
Hide until on file
Nobody asked

fit

Face blurred
cover
Face blurred
contain

badge

badge off

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-image

Use it for

  • Any photo of people, so a face with no consent on file never shows.
  • Tag each person with their consent and, where you know it, their face. A tagged face is blurred alone. With no face, the whole photo is.
  • Size its box. It fills it, and fit says whether the photo covers the box or fits inside it.
  • Pass blurredSrc, a copy the server blurred, wherever you can. The blur here is only a look, and the original still reaches the browser.
  • Pass your app's rule as photoConsent: who needs consent, and whether a photo without it is blurred or hidden.
  • Use MediaGrid for a set of photos and Lightbox to view one large.

Avoid it for

  • A person's profile photo. Use Avatar, which they chose to share.
  • Hiding a face from a screen reader only. The rule also hides it from sight, so pass every tagged person.
  • An image with no people in it, like a crest or a map. Use a plain img.

Props

  • item

    Required. The photo or video, with everyone tagged in it.

    MediaItem

  • MediaItem

    Type. src is the photo or the video's still. picture draws your own element in its place, like a next/image, with the same blur and hiding. aspect is width over height, so face blurs land before the photo loads. media defaults to video when video is set.

    { src?: string; picture?: ReactNode; aspect?: number; blurredSrc?: string; alt: string; caption?: string; media?: "photo" | "video"; video?: string; poster?: string; duration?: string; people?: MediaPerson[]; id?: string | number }

  • MediaPerson

    Type. Someone tagged in it. consent left out means missing. face is where their face is, as fractions of the photo.

    { person: string; photo?: string; consent?: "allowed" | "missing" | "withdrawn"; junior?: boolean; face?: { x: number; y: number; w: number; h: number } }

  • fit

    cover fills the box, cropped from the middle. contain fits the whole photo inside it.

    "cover" | "contain" · default "cover"

  • alt

    Replaces item.alt. Pass "" when something around it already names the photo.

    string

  • lazy

    Loads the photo as it nears the screen. Turn it off for the one photo a viewer shows.

    boolean · default true

  • photoConsent

    Your consent rule. who says whose face needs consent on file. missing says whether such a face is blurred or the photo hidden. Withdrawn consent always hides the photo.

    { who?: "everyone" | "juniors" | "nobody"; missing?: "blur" | "hide" } · default { who: "everyone", missing: "blur" }

  • badge

    A blurred photo says so in a small badge: Face blurred, Faces blurred, or Blurred. Turn it off when the words around it say so.

    boolean · default true

  • className

    Extra classes on the box. Later classes win.

    string

  • consentOf

    Function. What a photo may show: state clear, blurred, or hidden, who is missing, who withdrew, and whether the whole photo blurs.

    (item: MediaItem, rule?: PhotoConsentRule) => MediaConsent

Accessibility

  • The photo is named by item.alt, or by alt. Pass alt="" when a caption beside it already names it.
  • A blurred photo's badge is text, so screen readers hear it after the name.
  • A hidden photo loads nothing. Its box says Consent withdrawn, or Waiting for consent, in words.
  • The badge and each face's blur keep a border in forced-colors mode.

Examples

A photo with a junior who has no consent on file

<div className="size-48 overflow-hidden rounded-lg">
  <MediaImage
    item={{
      src: "/photos/team-offsite.jpg",
      alt: "Team photo at the offsite",
      people: [
        { person: "Josh Tait", consent: "allowed" },
        { person: "Ella Clarke", consent: "missing", junior: true, face: { x: 0.52, y: 0.2, w: 0.1, h: 0.14 } },
      ],
    }}
  />
</div>

Hide instead of blur, for juniors only

<div className="aspect-video w-full overflow-hidden rounded-lg">
  <MediaImage
    item={{ src: "/photos/summer-picnic.jpg", alt: "Summer picnic", people: [{ person: "Ella Clarke", junior: true }] }}
    fit="contain"
    photoConsent={{ who: "juniors", missing: "hide" }}
  />
</div>