Avatar Picker
Upload, change or remove a person's photo — a large avatar with a busy spinner, a file-picker button, Remove, and a line for a refused file.
- Status
- Since
0.23.44- Accessibility pattern
- named buttons, busy state, refusal announced and shown
Last updated
Install
Add Avatar Picker from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/avatar-pickerThe same command installs the registry items it composes: @vegastack/button, @vegastack/person-avatar, @vegastack/spinner, @vegastack/use-announcer, @vegastack/use-file-drop.
Usage
import { AvatarPicker } from "@/components/ui/avatar-picker";
<AvatarPicker
person={{ name: me.name, image: me.image, hue: me.color }}
busy={saving}
maxSize={10 * 1024 * 1024}
onSelect={(file) => uploadPhoto(file)}
onRemove={() => removePhoto()}
/>;AvatarPicker is where someone sets their own photo. It shows their PersonAvatar at 64px — the
photo, else their initials on their colour — beside "Upload photo", which opens the file browser
(on a phone, the browser offers the camera and the gallery). Once there is a photo the button reads
"Change photo" and "Remove" appears. A file of the wrong type or over maxSize is refused on the
line under the buttons and announced — an iPhone HEIC photo is told to export it as JPEG.
It only acquires the file: onSelect hands the app the one accepted File, and the app resizes
and uploads it, holds busy meanwhile, then passes the new person.image back.
Examples
With a photo
Busy
While busy, a spinner covers the avatar, the buttons are disabled and the root is aria-busy.
Error
error shows a failure the app found after the file was accepted — an image it could not read — on
the same line a refused file uses. size="xl" draws an 80px avatar.
IMG_2041.heic is a HEIC photo. Export it as JPEG and try again.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onSelect* | (file: File) => void | — | Called with the one accepted file. |
person* | Person | — | The person whose photo this is: the avatar, its initials and hue, and whether a photo exists (image). |
accept | string | "image/jpeg,image/png,image/webp" | The accepted MIME types, comma-separated, as on <input accept>. |
busy | boolean | false | An upload or removal is in flight: a spinner covers the avatar and the buttons are disabled. |
changeLabel | string | "Change photo" | The button label once there is a photo. |
error | React.ReactNode | — | An error from after the file was accepted (processing or upload), shown on the message line and announced (a string is announced each time it changes). |
maxSize | number | — | The largest file accepted, in bytes; a larger one is refused with a message. |
onRemove | (() => void) | — | Called by "Remove"; omit to hide it. It shows only while person.image is set. |
removeLabel | string | "Remove" | The remove button's label. |
size | "lg" | "xl" | "lg" | lg is a 64px avatar, xl 80px. |
uploadLabel | string | "Upload photo" | The button label while there is no photo. |
Data attributes and CSS variables on AvatarPicker
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "avatar-picker" | "avatar-picker-avatar" | "avatar-picker-busy" | "avatar-picker-message" | "avatar-picker-remove" | "avatar-picker-upload" |
Accessibility
- "Upload photo" / "Change photo" and "Remove" are real buttons; the file input behind them is hidden and never takes focus.
- One polite live region announces every refused file and every
error, exactly once each, with the same words shown under the buttons; the upload button is described by that line while it shows. - While
busythe root isaria-busyand the spinner reads "Loading".
| Key | Action |
|---|---|
| Tab | Move between Upload / Change photo and Remove |
| Enter / Space | Open the file browser, or remove the photo |
| Contract | States tested |
|---|---|
| Behaviour | empty, image, busy, rejected |
| Accessibility | labeled, browser-accessibility-test |
| Visual | default, busy, error |
Do / Don't
Dropzone
File acquisition — drop, browse, or paste — as a thin shell over the use-file-drop hook; the drop surface is the named, focusable control.
Text Edit
A Notion-style Tiptap markdown editor in body typography — slash and bubble menus, GFM tables, block drag handles, markdown shortcuts and an onCommit contract.