Skip to content
txbrendan/ui

Photo Field

beta

A person's own photo, changed in place: their photo at header size, then Change photo and Remove. A new photo opens in a circle to place, and saves as a 256px square.

Preview

photo

Profile photo

Your workspace and the teams you're on see it.

Mia's photo

Mia's manager and her team see it. Drop a photo on the circle, or add one.

size

Profile photo, sm

Profile photo, md

Profile photo, lg

states

Profile photo

Profile photo

Your admin manages this photo.

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/photo-field

Use it for

  • Every name has a photo. Put this in account settings, in joining, and in a guardian's sheet for their child.
  • Say who sees the photo in hint, like "Your workspace and the teams you're on see it."
  • A chosen file, or one dropped on the photo, opens in a circle to place. Drag it or use the arrow keys, and zoom with the buttons or + and -.
  • Use photo calls onValueChange with a 256px square JPEG data URL. Remove calls it with null. Upload it from there.
  • Saving and removing go on the page's ⌘Z stack for 8 seconds. ⌘Z calls onValueChange again with the old photo.
  • The photo changes where it is, so nothing else needs to say so. No toast.

Avoid it for

  • Other files, like documents or lists. Use a file drop.
  • Team badges or logos that must keep their shape. The photo is cut to a circle.
  • Choosing among photos already uploaded. Show them as choices instead.

Props

  • name

    Whose photo it is. Their initials show until there is a photo.

    string

  • value

    The photo now: a URL, or null for initials. Use it with onValueChange.

    string | null

  • defaultValue

    The photo at first render, when you do not control it.

    string | null · default null

  • onValueChange

    The new photo as a 256px square JPEG data URL, or null when removed. ⌘Z calls it again with the old one.

    (photo: string | null) => void

  • label

    Names the field, above it.

    ReactNode

  • hint

    Who sees the photo. It describes the field.

    ReactNode

  • error

    Replaces the hint, in red, and is announced once. A file that is not a photo shows its own error here.

    ReactNode

  • size

    The photo at 48, 56, or 64px, the circle to place it in, and the buttons.

    "sm" | "md" | "lg" · default "md"

  • disabled

    Blocks the buttons and drops.

    boolean · default false

  • className

    Classes for the frame. id and aria-* land on the group.

    string

Accessibility

  • The label names the group, and the hint or error describes it.
  • Change photo opens the file picker. A file can also be dropped on the photo.
  • Placing, focus moves to the circle. The arrow keys move the photo, Shift moves it further, + and - zoom, Enter uses it, and Esc puts it back without closing a sheet around it.
  • Zoom in and Zoom out are named buttons too, so placing never needs a drag.
  • Once saved or removed, focus returns to Change photo or Add a photo, and a polite status says what changed.
  • A file held over the photo shows an outline, which forced-colors mode draws too.

Examples

A profile photo, with who sees it

<PhotoField
  label="Profile photo"
  name="Riley Shaw"
  hint="Your workspace and the teams you're on see it."
  onValueChange={(photo) => console.log(photo)}
/>