File uploader
The drop target plus one progress row per file. Drag files onto it or click to pick them, and each transfer gets a row showing where it is: climbing, done, or failed with a retry. Presentational on purpose — it reports files through onFiles and the caller owns the transfer, which is what lets the same component serve a prototype that fakes progress and an app that really uploads.
Anatomy
A drop target stacked over its file rows, 16px apart. The target is a bordered card with the illustration, a bold call to action whose first word carries the primary tint, and the format and size rules underneath. Each file below is its own card.
Select a file or drag it here
Supports: JPG, JPEG, or PNG format
20 KB max file size.
- 1Drop target
rounded-xl border p-8, bg-card at rest. Clicking it opens the native picker; dropping on it fires onFiles.
- 2Illustration
The 64px upload mark from the Figma, in public/illustrations/uploader-drop.svg.
- 3Call to action
text-sm font-bold. The first word renders in primary as the click cue; the rest stays foreground.
- 4Constraints
text-sm muted, centered. Name the accepted formats and the size cap — this is the only place the merchant learns them before failing.
- 5Progress row
One UploadProgressCard per file: 40px type icon + name + status + a 2px bar, with a trailing X to remove it.
Usage
Select a file or drag it here
Supports: JPG, JPEG, or PNG format
20 KB max file size.
const [rows, setRows] = React.useState<Row[]>([]);
<FileUploader
multiple
accept="image/png,image/jpeg"
onFiles={(files) => setRows((prev) => [...prev, ...files.map(toRow)])}
>
{rows.map((r) => (
<UploadProgressCard
key={r.id}
name={r.name}
status={r.status}
value={r.value}
onRetry={() => retry(r.id)}
onRemove={() => remove(r.id)}
/>
))}
</FileUploader>Drop target states
At rest. Bordered card on bg-card, waiting for a drop or a click.
Select a file or drag it here
Supports: JPG, JPEG, or PNG format
20 KB max file size.
<FileUploader onFiles={handleFiles} />A file is over the target: the border turns primary and the surface picks up a faint primary wash. The dragged file's own ghost is drawn by the browser, not by this component.
Select a file or drag it here
Supports: JPG, JPEG, or PNG format
20 KB max file size.
// Owned by the component — drag a file over the target to see it.The limit is reached. The target dims to 50% and stops accepting input — and the copy changes to say why, instead of leaving a dead target with no explanation.
Image uploaded
You can only add one image or file.
<FileUploader
disabled
title="Image uploaded"
description="You can only add one image or file."
/>Progress row states
The bar climbs and the percentage sits on the right of the status line.
<UploadProgressCard name="Beats-Solo3-Wireless.png" value={67} onRemove={remove} />The bar fills in success green and a filled CheckCircle replaces the percentage.
<UploadProgressCard name="Beats-Solo3-Wireless.png" status="success" onRemove={remove} />The status line turns destructive and a retry control appears next to it. This is one of the legitimate uses of the destructive token: it signals a state, it is not a red button.
<UploadProgressCard
name="Beats-Solo3-Wireless.png"
status="error"
onRetry={retry}
onRemove={remove}
/>The resting row. The transfer is history, so the bar disappears entirely and only the file and its remove action remain.
<UploadProgressCard name="Beats-Solo3-Wireless.png" status="done" onRemove={remove} />File types
The extension picks the icon. The Figma's icon set is Phosphor too, so this is a straight mapping rather than a vendored icon pack — pdf, png, jpg, svg, ppt, xls, csv, doc, zip and the code types all resolve; anything unknown falls back to the generic File. Pass src instead to show a real thumbnail for image uploads.
<UploadProgressCard name="refunds_2025.xls" status="done" />
<UploadProgressCard name="payouts.csv" status="done" />
<UploadProgressCard name="avatar.png" status="done" src={previewUrl} />Import
Copy this import line at the top of the file where you compose this molecule.
import { FileUploader, UploadProgressCard } from "@/components/patterns/file-uploader";Props
Everything else is forwarded to the underlying elements via ...props.
FileUploader
| Prop | Type | Default | Description |
|---|---|---|---|
| onFiles | (files: File[]) => void | — | Fires with the dropped or picked files. The component never uploads anything itself. |
| accept | string | — | Native accept string, e.g. "image/png,image/jpeg". |
| multiple | boolean | false | Allow picking more than one file at a time. |
| disabled | boolean | false | Dims the target to 50% and stops input. Use it once the limit is reached, and change the copy to say why. |
| title | string | 'Select a file or drag it here' | Bold line. Its FIRST WORD renders in primary as the click cue. |
| description | ReactNode | formats + size copy | Muted line under the title. Name the accepted formats and the size cap. |
| children | ReactNode | — | Rendered under the drop target, 16px below it. Normally the UploadProgressCards. |
| className | string | — | Extra classes on the outer column. |
UploadProgressCard
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string (required) | — | File name shown as the row title. Its extension also picks the icon. |
| status | "uploading" | "success" | "error" | "done" | "uploading" | Which row to render. "done" drops the bar entirely. |
| value | number | 0 | 0-100. Only read while uploading — success fills the bar and done hides it. |
| src | string | — | Thumbnail URL. Replaces the file-type icon with the image itself. |
| label | string | — | Status line override. Each status ships sensible default copy. |
| onRetry | () => void | — | Renders the retry control on the error row. Omit it and the row just reports the failure. |
| onRemove | () => void | — | Renders the trailing X. Omit it for a row the merchant cannot dismiss. |
| className | string | — | Extra classes on the card. |
Related
Cross-links to the atoms this molecule composes and sibling patterns.
- ProgressThe bar inside each row. The uploader sets it to 2px and tints the indicator per status.
- Empty stateSibling for the case where there is nothing yet AND nothing to drop — the uploader is for when the merchant is meant to add something.
- InputFor a plain file field with a Choose file button, when a full drop target is more than the form needs.