Photo Field
betaA 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
size
states
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/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
| Prop | Type | Default | What it does |
|---|---|---|---|
| name | string | - | Whose photo it is. Their initials show until there is a photo. |
| value | string | null | - | The photo now: a URL, or null for initials. Use it with onValueChange. |
| defaultValue | string | null | null | The photo at first render, when you do not control it. |
| onValueChange | (photo: string | null) => void | - | The new photo as a 256px square JPEG data URL, or null when removed. ⌘Z calls it again with the old one. |
| label | ReactNode | - | Names the field, above it. |
| hint | ReactNode | - | Who sees the photo. It describes the field. |
| error | ReactNode | - | Replaces the hint, in red, and is announced once. A file that is not a photo shows its own error here. |
| size | "sm" | "md" | "lg" | "md" | The photo at 48, 56, or 64px, the circle to place it in, and the buttons. |
| disabled | boolean | false | Blocks the buttons and drops. |
| className | string | - | Classes for the frame. id and aria-* land on the group. |
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)}
/>