File upload is the interface where the happy path is five minutes of work and everything else is the rest of the week. A drop zone that accepts a file is easy. A drop zone that survives a 200MB video on hotel wifi, a HEIC photo from an iPhone, a folder dragged instead of a file, and a reader who closes the tab halfway through is not.
114 components on 21st carry the file-upload tag. Here is what each shape covers and what none of them do.
The drop zone
The main shape: a bordered area that accepts a drag or opens the file picker. File Upload by Manu Arora and BeUI File Upload by Saurabh Chauhan are the general versions, and File Upload Card by Ravi Katiyar is the card-shaped one.
The requirement that gets missed: a drop zone must also be a button. Dragging is a pointer gesture with no keyboard or touch equivalent, so the same area needs a real <input type="file"> behind a label, focusable and activated by Enter. A div with onDrop and nothing else excludes phones and keyboards entirely.
Two more. Highlight on dragover and clear it on dragleave and drop, because a zone that stays highlighted reads as stuck. And accept a paste: Ctrl+V with an image in the clipboard is how a large share of screenshots arrive.
The typed uploaders
Avatar Uploader by Efferd adds cropping, which is not optional for avatars, as the avatar guide argues. Video Upload Card by Isaiah is the large-file case, and useImageUpload by originui is the hook rather than the visual, covering selection, preview and progress.
The file list
File Card Collections by Faisal Amir is the after state, which most components skip: what the uploaded set looks like once it exists, with per-file status and removal.
Google Drive Uploader Toast by Suraj Gaud is the background-progress pattern, where the upload continues in a corner while the reader gets on with something else. For anything slow, that is the right shape.



The cases that decide whether it works
Validate before the request. Type and size are checkable in the browser, and a 200MB file rejected after a four-minute upload is the worst possible ordering. The accept attribute filters the picker; it does not stop a drag, so check again in code.
Validate on the server anyway. The client check is for the reader's benefit, not for security. A content-type header is whatever the client says it is.
Progress, and a cancel. XMLHttpRequest still exposes upload progress more simply than fetch does, and an upload with no cancel is a tab someone has to close.
The formats you did not plan for. iPhones produce HEIC, which most browsers will not render in a preview. Either convert server-side or say clearly that the format is unsupported, rather than showing a broken image.
A folder, or twenty files. Dragging a folder yields directory entries rather than files, and a multi-select of twenty needs a queue with per-file state, not one spinner.
The error that says what to do. "Upload failed" is not a message. Too large, wrong type, network dropped and try again are four different sentences.
Where it should go
Direct to storage, with a presigned URL, for anything more than a few megabytes. Routing a large file through your own server costs you the bandwidth, the memory and a timeout on the serverless platform most of these products run on.
The shape is: the client asks your API for a signed URL, uploads straight to the bucket, then tells your API it finished. It is more moving parts than a form post and it is the difference between a feature that works at 200MB and one that fails at 10.




Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
<input type="file"> | The picker, the keyboard path and the accept filter, free | No drag, no progress, no preview |
| Uppy | Resumable uploads, providers, retries and a full UI | A large dependency with its own look |
| A storage provider's SDK | Presigned URLs, multipart and resume | Ties the client to that provider |
| 21st | Drop zones, avatar croppers, file lists and background progress | Quality varies by author, so preview before you take it |
Taking one
Every component page has a live preview and the code. Installing goes through the shadcn CLI against our registry:
That key comes from your 21st account, and installs require a membership. Set API_KEY_21ST once in your shell and the command works for anything in the catalogue. The 21st MCP covers the same ground without leaving the editor: ask Claude, Cursor or Codex for an uploader, get the previews inline, and let the agent write the file.
Browse file upload components →
Frequently asked
- How do you make a drop zone accessible?
- By making it a button as well as a drop target. Dragging is a pointer gesture with no keyboard or touch equivalent, so the same area needs a real file input behind a label, focusable and activated by Enter. Also accept paste, because a large share of screenshots arrive through Ctrl+V.
- Where should validation happen for uploads?
- Both places, for different reasons. In the browser before the request, so a 200MB file is not rejected after a four-minute upload; the accept attribute filters the picker but does not stop a drag, so check in code too. On the server regardless, because a content-type header is whatever the client claims.
- Should uploads go through my server?
- Not for anything more than a few megabytes. Use a presigned URL: the client asks your API for one, uploads straight to storage, then tells your API it finished. Routing a large file through your own server costs bandwidth, memory and usually a timeout on serverless platforms.
- Why do iPhone photos fail to preview?
- They are HEIC, which most browsers will not render. Either convert server-side or say clearly that the format is unsupported, rather than showing a broken image. It is one of a handful of cases, alongside dragged folders and twenty-file multi-selects, that only appear once real people use the feature.