Patterns

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.

Updated Aug 11, 2026 by Juan Pablo Turina

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.

Beats-Solo3-Wireless.png
Uploading...67%
  1. 1
    Drop target

    rounded-xl border p-8, bg-card at rest. Clicking it opens the native picker; dropping on it fires onFiles.

  2. 2
    Illustration

    The 64px upload mark from the Figma, in public/illustrations/uploader-drop.svg.

  3. 3
    Call to action

    text-sm font-bold. The first word renders in primary as the click cue; the rest stays foreground.

  4. 4
    Constraints

    text-sm muted, centered. Name the accepted formats and the size cap — this is the only place the merchant learns them before failing.

  5. 5
    Progress row

    One UploadProgressCard per file: 40px type icon + name + status + a 2px bar, with a trailing X to remove it.

Usage

FileUploader renders the target and whatever you pass as children underneath. Keep the file list in your own state and map it to UploadProgressCards — progress, retry and removal are yours to drive.

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

Default

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} />
Dragging

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.
Disabled

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

Uploading

The bar climbs and the percentage sits on the right of the status line.

Beats-Solo3-Wireless.png
Uploading...67%
<UploadProgressCard name="Beats-Solo3-Wireless.png" value={67} onRemove={remove} />
Success

The bar fills in success green and a filled CheckCircle replaces the percentage.

Beats-Solo3-Wireless.png
Upload completed
<UploadProgressCard name="Beats-Solo3-Wireless.png" status="success" onRemove={remove} />
Error

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.

Beats-Solo3-Wireless.png
Upload failed
<UploadProgressCard
  name="Beats-Solo3-Wireless.png"
  status="error"
  onRetry={retry}
  onRemove={remove}
/>
Done

The resting row. The transfer is history, so the bar disappears entirely and only the file and its remove action remain.

Beats-Solo3-Wireless.png
Uploaded
<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.

report.pdf
Uploaded
refunds_2025.xls
Uploaded
payouts.csv
Uploaded
archive.zip
Uploaded
notes.doc
Uploaded
app.tsx
Uploaded
<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.

FileUploader renders the target and whatever you pass as children underneath. Keep the file list in your own state and map it to UploadProgressCards — progress, retry and removal are yours to drive.
import { FileUploader, UploadProgressCard } from "@/components/patterns/file-uploader";

Props

Everything else is forwarded to the underlying elements via ...props.

FileUploader

PropTypeDefaultDescription
onFiles(files: File[]) => voidFires with the dropped or picked files. The component never uploads anything itself.
acceptstringNative accept string, e.g. "image/png,image/jpeg".
multiplebooleanfalseAllow picking more than one file at a time.
disabledbooleanfalseDims the target to 50% and stops input. Use it once the limit is reached, and change the copy to say why.
titlestring'Select a file or drag it here'Bold line. Its FIRST WORD renders in primary as the click cue.
descriptionReactNodeformats + size copyMuted line under the title. Name the accepted formats and the size cap.
childrenReactNodeRendered under the drop target, 16px below it. Normally the UploadProgressCards.
classNamestringExtra classes on the outer column.

UploadProgressCard

PropTypeDefaultDescription
namestring (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.
valuenumber00-100. Only read while uploading — success fills the bar and done hides it.
srcstringThumbnail URL. Replaces the file-type icon with the image itself.
labelstringStatus line override. Each status ships sensible default copy.
onRetry() => voidRenders the retry control on the error row. Omit it and the row just reports the failure.
onRemove() => voidRenders the trailing X. Omit it for a row the merchant cannot dismiss.
classNamestringExtra 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.