Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
Since
0.23.44
Accessibility pattern
named buttons, busy state, refusal announced and shown

Last updated

AR

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

The 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.

AR

Examples

With a photo

AL

Busy

While busy, a spinner covers the avatar, the buttons are disabled and the root is aria-busy.

LO

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.

YT

IMG_2041.heic is a HEIC photo. Export it as JPEG and try again.

API Reference

PropTypeDefaultDescription
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).
acceptstring"image/jpeg,image/png,image/webp"The accepted MIME types, comma-separated, as on <input accept>.
busybooleanfalseAn upload or removal is in flight: a spinner covers the avatar and the buttons are disabled.
changeLabelstring"Change photo"The button label once there is a photo.
errorReact.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).
maxSizenumber—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.
removeLabelstring"Remove"The remove button's label.
size"lg" | "xl""lg"lg is a 64px avatar, xl 80px.
uploadLabelstring"Upload photo"The button label while there is no photo.

Data attributes and CSS variables on AvatarPicker

AttributeValues
data-sizemirrors 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 busy the root is aria-busy and the spinner reads "Loading".
KeyAction
TabMove between Upload / Change photo and Remove
Enter / SpaceOpen the file browser, or remove the photo
ContractStates tested
Behaviourempty, image, busy, rejected
Accessibilitylabeled, browser-accessibility-test
Visualdefault, busy, error

Do / Don't

Do
Hold busy until the new photo URL is back, then pass it as person.image.
Don't
Use AvatarPicker to attach files to a record — that is Dropzone with Attachment.

On this page