PNG, JPG or WebP, up to 10 MB⌘V pastes one
Try
"use client"
import { WavePhotoUpload } from "@/components/keyframery/wave-photo-upload"
export function CoverPhoto() {
return (
<WavePhotoUpload
upload={(file, report, signal) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open("POST", "/api/photos")
xhr.upload.onprogress = (e) => e.lengthComputable && report(e.loaded / e.total)
xhr.onload = () => (xhr.status < 300 ? resolve(xhr.response) : reject(new Error(xhr.statusText)))
xhr.onerror = () => reject(new Error("The upload failed"))
signal.addEventListener("abort", () => xhr.abort())
const body = new FormData()
body.append("photo", file)
xhr.send(body)
})
}
onChange={(file) => console.log(file)}
/>
)
}Installation
npx shadcn add @keyframery/wave-photo-upload
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
| upload | (file, report, signal) => Promise | — | Sends the photo. Call report with 0 to 1 as it goes and throw if it fails. Leave it out and the photo prints at its own pace. |
| onChange | (file: File | null) => void | — | Runs with the photo once it's in, and with null when it's removed. |
| accept | string[] | ["image/png", "image/jpeg", "image/webp"] | The file types it takes. The error message lists them by name. |
| maxSize | number | 10 MB | The largest file it takes, in bytes. |
| aspect | number | — | Keeps the frame one shape (width ÷ height), like 1 for an avatar. By default it takes each photo's shape. |
| disabled | boolean | false | Stops it taking photos. |
| ref | { pick(file), clear() } | — | Start with a file you already have, as if it had been dropped, or remove the photo. |
Accessibility and motion
- Underneath it's a real file field: the frame is a button that opens the file picker, and screen readers hear the progress and the result.
- fetch can't report upload progress yet, so the example uses XMLHttpRequest. Anything that calls report with 0 to 1 will do.
- With reduced motion on, nothing moves: the photo simply fades in as it uploads.