A profile photo or logo with its upload: pick or drop a picture, crop it in a dialog, get a square file back.
$ pnpm dlx shadcn@latest add @dashral/avatar-uploadInstallation
$ pnpm dlx shadcn@latest add @dashral/avatar-uploadAdds the component, the components it builds on, its npm packages and the theme tokens it uses. Or by its full URL: https://dashral.com/r/avatar-upload.json
When to use
- Someone's profile photo, or a workspace or company logo, in a settings form: pick or drop a picture, crop it, upload a small square file.
When not to use
- Several files, or files that aren't pictures: File Dropzone.
- A crop of another shape, such as a 3:1 cover: Image Cropper with cropImage.
Usage
defaultValue is the photo you have; onValueChange gets the cropped 512 px File to upload, or null when it's removed.
"use client"
import { AvatarUpload } from "@/components/dashral/avatar-upload"
export default function ProfilePhoto() {
return (
<AvatarUpload
label="Profile photo"
description="JPG or PNG, up to 5 MB."
defaultValue="https://picsum.photos/id/64/400/400"
maxSize={5 * 1024 * 1024}
onValueChange={(file) => console.log("upload", file)}
/>
)
}Using the npm package? Import from @dashral/ui/components/avatar-upload instead, here and in the examples; the rest is the same.
Examples
API reference
AvatarUpload
| Prop | Type | Default |
|---|---|---|
value / defaultValue | File | string | nullThe picture now: a URL (from your server, say) or the File cropped last. | null |
onValueChange | (file: File | null) => voidThe cropped picture, ready to upload, or null when it's removed. | — |
accept | stringLike the accept attribute; only pictures are taken. | "image/*" |
maxSize | numberLargest picture someone can pick, in bytes, before cropping. | — |
shape | "circle" | "square"A circle for a person, a square for a logo; the file is square either way. | "circle" |
fallback | ReactNodeIn the picture's place when there's none, e.g. initials. | a person icon |
outputSize | numberWidth and height of the file, in pixels. | 512 |
outputType | string"image/png" keeps a logo's transparency; "image/webp" is smaller. | "image/jpeg" |
cropTitle / cropDescription | ReactNodeThe crop dialog's title and the line under it. | — |
name | stringSends the cropped file with a form, as a file input of that name would. It's empty when nothing new was cropped: tell a removal apart with onValueChange. | — |
disabled / required / id | — | |
label | ReactNodeShown above, linked to the control. | — |
description | ReactNodeA hint under the control. | — |
error | boolean | ReactNodetrue shows the error style; a message also shows under the control. | — |
labelAction | ReactNodeAt the end of the label's row, e.g. a “Forgot password?” link or the current value. | — |
withAsterisk | booleanMarks the label with an asterisk, e.g. alongside required. | false |
classNames | { root, label, description, error, preview, actions }Classes for each part. | — |
className | stringExtra classes, merged with cn() so yours win. | — |
Keyboard
| Keys | Action |
|---|---|
| Tab | Focus Upload (or Change) photo, then Remove |
| EnterorSpace | Open the file picker |
| In the dialog | As on Image Cropper; Escape cancels |
Accessibility
- The buttons say what they do; the preview beside them is for the pointer and for a dropped file, so it's hidden from screen readers.
- The whole control is role="group", named by its label and described by its description.
- A refused file is said in an alert, with the reason, and the Upload button is described by it.
- A picture that can't be opened or cropped says so in the dialog, and Apply waits until the picture has loaded.
Motion
- A new picture fades in over the old one and a removed one fades out (--duration-standard), so the preview never blinks empty.
Related blocks
Whole sections built with Avatar Upload or made for the same job, ready to drop in. Free blocks come with their code; Pro blocks come with Dashral Pro.

The workspace's name, logo, cover and colour, previewed live in the sidebar, the sign-in page, emails and invoices.

A profile form with a photo cropped as it's picked, beside a card that shows how the profile looks to others as you type.

