Avatar Upload
Let people upload and crop a profile photo before saving it.
import { AvatarUpload } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use it on a profile or account page, wherever one person or workspace has one photo. The avatar itself is the button and a drop target, and a picked image opens a crop dialog, so what's saved is a square that fills the circle instead of a stretched or off-center photo. For attaching files of any kind, or several at once, use File Upload. To only show a photo, use Avatar.
import { AvatarUpload } from '@youj/react'
export function ProfilePhoto({ user }: { user: User }) {
return (
<AvatarUpload
src={user.avatarUrl}
name={user.name}
fallback={user.initials}
onSave={async (file) => {
const url = await uploadAvatar(file)
await updateUser({ avatarUrl: url })
}}
onRemove={() => updateUser({ avatarUrl: null })}
/>
)
}onSave receives the crop as a square File, outputSize pixels on a side (512 by default) in outputType (JPEG by default). It runs through AsyncButton's onAction: Save shows a spinner, then a drawn check, and the dialog closes itself. Reject to show the error. The saved crop shows at once, until you pass a new src. onRemove adds a Remove button while there's a photo.
Description
A 72px avatar whose save handler waits a moment, then shows the cropped photo; its remove handler clears it.
Drag the photo to move it, and zoom from 1 to 4 times with a pinch, the scroll wheel or the Zoom slider. Zoom is anchored on the circle's center. The photo follows the pointer exactly, and pulled past an edge it gives with resistance and springs back on release, so the circle is always filled. A rule-of-thirds grid shows while it moves.
From the keyboard, the arrow keys move the photo, Shift moves it further, + and − zoom, and 0 resets.
The same upload at 48 and 96 pixels, and a disabled one.
size sets the avatar's edge in pixels, 64 by default. accept defaults to PNG, JPEG and WebP, and maxSize to 10 MB. A file that breaks a rule is explained in a Field error under the avatar and never opens the cropper.
Code
avatar-upload-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsonSave, src, fallback… (12)
Props
| Prop | Type | |
|---|---|---|
src | string | nullThe current photo. | – |
fallback | ReactNodeShown when there is no photo (initials). | – |
name | stringNames the photo for assistive tech, e.g. the person's name. | – |
onSaveRequired | (file: File) => unknownReceives the cropped square image. Resolve when saved; reject to show the error. | – |
onRemove | () => unknownShows a Remove button while there is a photo. | – |
accept | stringdefault 'image/png,image/jpeg,image/webp' | 'image/png,image/jpeg,image/webp' |
maxSize | numberIn bytes. default 10 * 1024 * 1024 | 10 * 1024 * 1024 |
outputSize | numberOutput edge in px. default 512 | 512 |
outputType | 'image/jpeg' | 'image/png' | 'image/webp'default 'image/jpeg' | 'image/jpeg' |
size | numberAvatar edge in px. default 64 | 64 |
disabled | booleandefault false | false |
className | string | – |
cropRegion is exported for doing the same crop on a server.
MotionHover, focus or drag over, crop dialog, drag… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Hover, focus or drag overa scrim and the camera icon rise over the photo | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
Crop dialogopens and closes as every Dialog (yj-dialog) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Dragthe photo tracks the pointer 1:1, with resistance past an edge | no token no easing | no easing | |
| Release past an edgesprings back so the circle stays filled | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Thirds gridfades in while the photo moves | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| SaveAsyncButton: spinner, then a drawn check | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) |
Reduced motion
The photo clamps at the circle's edge instead of springing back, the scrim appears at once and the dialog opens without motion.
Reduced motion
The scrim and camera appear at once, the dialog opens without its rise and scale, and the photo stops at the circle's edge instead of springing back. Saving still shows its states.
AccessibilityKeyboard, focus, announcements
Accessibility
- The avatar is a button named "Upload photo", or "Change photo" once there is one, and described by the rules line beside it. An Upload or Change button beside it does the same, for people who don't expect the photo to be clickable.
- The crop area is a focusable group named "Photo position and zoom" with its keyboard instructions as its description.
- The Zoom slider and the zoom buttons are named, and the buttons disable at 1× and 4×.
- The dialog is titled "Crop your photo" and describes how to move and zoom. Removing the photo moves focus back to the avatar.
- A rejected file is explained in a Field error, which is announced.