File Upload
Upload files with clear rules, validation, progress and retry controls.
import { FileRejection, FileUpload, UploadContext, UploadFile, UseFileUploadOptions } 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 wherever people attach files: receipts on an expense, screenshots on a bug report, a spreadsheet to import, photos for a listing. The zone states the rules before anyone picks a file, a rejection says which file broke which rule, and each accepted file shows its own progress, a check when it's done, or the error with a Retry. For a single profile photo with cropping, use Avatar Upload.
import { FileUpload } from '@youj/react'
export function Attachments() {
return (
<FileUpload
accept=".png,.jpg,.jpeg,.pdf"
maxSize={5 * 1024 * 1024}
maxFiles={6}
onUpload={(file, { signal, onProgress }) => uploadToStorage(file, { signal, onProgress })}
/>
)
}The zone is a button and a drop target: click it, press Enter or Space, or drag files onto it. Its second line is generated from the rules, such as "PNG, JPG or PDF · up to 5 MB each", and hint replaces it. title sets the main line.
Description
A card of attachments that takes PNG, JPEG and PDF files up to 5 MB, six at most, uploaded by a simulated endpoint that fails every third file.
onUpload(file, { signal, onProgress }) uploads one file. Report progress from 0 to 100 through onProgress, resolve when the upload is done, and reject or throw to show the error on the row with a Retry button. Removing a file while it uploads aborts signal, so pass it on to fetch or your storage client. Try the preview: every third file fails.
Files are checked against accept (extensions, MIME types or wildcards like image/*), maxSize in bytes, maxFiles and duplicates. Every rejected file is listed with its reason in one alert that grows in under the zone, so dropping ten files at once gives one explanation and not ten. multiple={false} keeps the picker to one file.
The gallery variant for images up to 8 MB, eight at most, with simulated upload progress.
variant="gallery" is for images: accepted files become square tiles in a grid with their thumbnails, and once there are files, the zone becomes the last tile.
A single-spreadsheet picker with no upload handler, so the file is only collected and a form button imports it.
Without onUpload, files are only collected. Read them from onFilesChange and send them with the form, as this import does. useFileUpload exposes the same state and rules for a layout of your own, and useFileDrop the drop target alone.
Code
file-upload-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsvariant, multiple, title… (10)
Props
| Prop | Type | |
|---|---|---|
variant | 'list' | 'gallery''list' (default): rows with progress. 'gallery': a grid of image tiles. default 'list' | 'list' |
multiple | booleanAllow several files. default true | true |
disabled | booleandefault false | false |
title | ReactNodeThe zone's main line. | – |
hint | ReactNodeOverrides the generated rules line ("PNG or JPG · up to 5 MB each"). | – |
accept | stringAs the input's | – |
maxSize | numberIn bytes. | – |
maxFiles | numberTotal files allowed. | – |
onUpload | (file: File, context: UploadContext) => Promise<unknown>Uploads one file. Resolve when done; reject (or throw) to show the error. Without it, files are only collected. | – |
onFilesChange | (files: UploadFile[]) => void | – |
Other props pass through: extends ComponentProps<'div'>. | ||
The onUpload, onFilesChange, accept, maxSize and maxFiles options are the same for useFileUpload. Each file in onFilesChange is an UploadFile: the File, its status (uploading, done or error), progress, error and an image preview URL.
MotionDrag over, file joins, rows make room… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Drag overthe zone’s border turns accent and the upload icon lifts | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| File joinsthe row fades in with a 6px drop while the rows below glide (Presence List) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Rows make room | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Progressthe bar glides to each report, retargeting mid-glide | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Donethe row settles into a drawn check | duration.slowease.enter340ms · cubic-bezier(0.16, 1, 0.3, 1) | 340mscubic-bezier(0.16, 1, 0.3, 1) | |
| Rejectionsthe alert grows in under the zone, as a Field error does | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| File removedfades in place, then the rest close up | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) |
Reduced motion
Rows and tiles appear and go at once, progress jumps to each value and the check is already drawn.
Reduced motion
Rows and tiles appear and disappear without fading or gliding, progress bars jump to each value and the done check shows already drawn. Drag-over still changes the border.
AccessibilityKeyboard, focus, announcements
Accessibility
- The drop zone is a real button, so Enter and Space open the file picker, and its description reads the rules.
- Every change is announced politely: files added, rejections, each upload finishing or failing, and removals.
- Rejections are listed through a Field error under the zone, one line per file with its reason.
- Each row's buttons are named with the file name: "Cancel upload of receipt.pdf" while it uploads, then "Remove receipt.pdf", and "Retry receipt.pdf" after a failure.
- Focus never drops to the page: removing a row moves focus to the next row's button, or back to the zone when the list empties.