Skip to content

Avatar Upload

Let people upload and crop a profile photo before saving it.

import { AvatarUpload } from '@youj/react'

Installation & usage

npm i @youj/react

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

Reference

PropsonSave, src, fallback… (12)

Props

AvatarUpload props
PropType
srcstring | null

The current photo.

–
fallbackReactNode

Shown when there is no photo (initials).

–
namestring

Names the photo for assistive tech, e.g. the person's name.

–
onSaveRequired(file: File) => unknown

Receives the cropped square image. Resolve when saved; reject to show the error.

–
onRemove() => unknown

Shows a Remove button while there is a photo.

–
acceptstring

default 'image/png,image/jpeg,image/webp'

'image/png,image/jpeg,image/webp'
maxSizenumber

In bytes.

default 10 * 1024 * 1024

10 * 1024 * 1024
outputSizenumber

Output edge in px.

default 512

512
outputType'image/jpeg' | 'image/png' | 'image/webp'

default 'image/jpeg'

'image/jpeg'
sizenumber

Avatar edge in px.

default 64

64
disabledboolean

default false

false
classNamestring–

cropRegion is exported for doing the same crop on a server.

MotionHover, focus or drag over, crop dialog, drag… · reduced motion

Motion

Motion tokens
MomentTokens
Hover, focus or drag overa scrim and the camera icon rise over the photo
duration.fastease.standard

140ms · 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.enter

220ms · 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.smooth

0.36s, bounce 0

0.36s, bounce 0
Thirds gridfades in while the photo moves
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
SaveAsyncButton: spinner, then a drawn check
duration.slowease.emphasized

340ms · 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.