File upload
A zone to choose or drop files, and the list of what became of them: uploading, done, failed with a reason. The component checks type and size and does no network of its own.
Try it
Drop files on the zone, or press it and choose. The upload here is pretend: the bar climbs, and every third file fails so Retry has something to do. Drop a .txt to see a refusal.
Drawn as the product’s “Upload image” dialog, in three states: at rest, dragged over, and uploading; and as the Uploaded Document card, with the bar the Progress already draws. Underneath is a real input type="file" and the zone is its label, so Tab reaches it and Enter opens the picker; accept and multiple are the input’s. What is chosen or dropped is checked against accept and maxSize, the accepted files go to onAdd, and the refused ones are shown as failed cards with the reason, removable, without the caller writing a word.
The list is the caller’s
files is what the caller knows: each with a status, ready (chosen and not sent, for a form that sends on save), uploading with a progress or without one, done with an href for Download, or failed with an error and Retry. The component draws it and asks; the caller uploads, retries and removes. One component serves an upload per request and a form that sends everything at the end.
The drawn single-file zone
Without multiple the zone takes one file and, as drawn, shows the upload inside itself, the Loader in the circle and “Uploading” with the name; done, the card appears below and the zone is ready for the next.
Compact, and the tile
compact is the same input as a button-like label with the hint beside, and the whole root takes a drop. tile is the drawn “+” with its words off screen; the row of documents around it is the page’s, and its list is left to the page too.
States
- Justin - xray31rt.pdf
- Consent form, signed on the 12th of September.pdf
- Referral.pdf
- scan.heicThe server refused it ·
- notes.txt
Where it departs from the drawing, and why
The drawn edge is a light dashed grey at about 1.5:1. The roadmap has the drop zone’s dashed edge as a boundary, and a boundary needs 3:1 (WCAG 1.4.11), so it is border/strong, the Switch’s and the Slider’s token; dragged over, the zone is surface/accent-subtle with a border/accent edge, the pair the Table’s stripe already holds. The zone at rest is surface/sunken, which on a card is a well and in dark is the canvas, where the edge alone carries it, as the Tabs’ track. The tag-like card in the product, “PDF . Download”, keeps its words, with Download as the Link.
Best practice, and the alternatives
The zone above and the files below is what Carbon, Atlassian and Polaris converged on: the zone stays ready while files climb, and each file says its own state. Show the limits before the choice, in the hint, and refuse in the list rather than in a dialog. Keep the words that open the picker as words: an icon alone says nothing to someone who has not dropped a file before. A single-file zone that shows the upload inside itself is the drawing’s choice and reads well in a dialog; with more than one file it hides the zone at the moment a second drop is likely, which is why multiple keeps the zone and lists the cards. The tile is for a row the page already lays out.
Accessibility
The input is named by the words, “Choose files or drag and drop”, and described by the hint. The zone draws the ring when the input inside it has focus. Each card’s × is “Remove” and the file’s name; Retry is a button in the error’s line; the bar is the Progress, named “Uploading” and the name. Dragging is an addition, never the only way: the picker is always there.
Measures
| Part | Value |
|---|---|
| Zone | min 200 tall, 24 padding, a dashed hairline at radius lg on surface/sunken |
| Icon | 56 circle on surface/raised at elevation/sm, the cloud at 32 in text/accent |
| Words | body/md Medium; the words that open the picker in text/accent, underlined |
| Hint | caption/md in text/tertiary |
| Card | 8 12 on surface/raised, a border/subtle hairline at radius md; border/danger when failed |
| Card icon | 40 box on surface/sunken at radius sm, the document at 24 |
| Tile | 64 square (--file-upload-tile), the same dashed edge at radius md |
| Compact | a 32 button-like label with the hint beside |
Using it
import { FileUpload } from 'alpenglow';
import type { UploadFile } from 'alpenglow';
const [files, setFiles] = useState<UploadFile[]>([]);
<FileUpload
multiple
accept="image/*,.pdf"
maxSize={5_000_000}
hint="PNG, JPEG, GIF or PDF under 5 MB."
files={files}
onAdd={(chosen) => {
// The component checked type and size; the caller uploads.
for (const file of chosen) upload(file, (progress) => setFiles(…));
}}
onRemove={(file) => setFiles((f) => f.filter((x) => x.id !== file.id))}
onRetry={(file) => upload(file)}
/>
// A dense form: the button and the hint on one line, dropping on the whole root.
<FileUpload variant="compact" accept=".pdf" hint="PDF only." onAdd={…} />
// The drawn "+" at the end of a row of documents.
<FileUpload variant="tile" label="Add a document" onAdd={…} />| Prop | Type | Default |
|---|---|---|
onAdd | (files: File[]) => void | required |
files | Upload | [] |
onRemove / onRetry | (file: Upload | — |
onReject | ({ file, reason: "type" | "size" }[]) => void | — |
accept | string | — |
maxSize | number (bytes) | — |
multiple | boolean | false |
variant | 'zone' | 'compact' | 'tile' | 'zone' |
label | string | "Choose a file" / "Choose files" |
dropLabel | string | 'or drag and drop' |
hint | React | — |
disabled | boolean | false |
name | string | — |
removeLabel / retryLabel / downloadLabel / uploadingLabel | string | Remove / Retry / Download / Uploading |
rejectionLabel | (reason, file) => string | "Not a supported type" / "Larger than 5 MB" |
className | string | — |