File upload
Choose files and watch them arrive: a FileUpload object with five parts, three item states, five zone states — and no uploading of its own.
File upload
Choose files and watch them arrive: a FileUpload object with five parts, three item states, five zone states — and no uploading of its own.
Demo
Three parts take props worth turning: the zone and the two items. Pick one, change its props, and copy the result.
SVG, PNG, JPG or GIF (max. 800x400px)
The kit uploads nothing
DropZone hands accepted files to onDropFiles once and stops there; the items draw the progress you pass and advance nothing. The playground has no upload behind it, so dropping a file on its zone does nothing visible, and the copied snippet names the handler you have to wire. The contextual demo below runs a real queue on a timer.Contextual demo
An uploader is a dialog or a settings row most of the time: three of them here, with a queue that advances on its own.
Six consumers already ship it
file-upload-modal renders the zone and a queue of bar items, file-upload-menu the zone and three fill items, new-project-modal a single-image zone, form-02-modal and profile-settings-modal the zone beside an avatar, the first with its icon hidden, and new-message-filled-modal only getReadableFileSize, for its attachment chips. The fixtures on this page are the slideout’s three files.The family
One FileUpload object with five parts plus a size formatter, from one file. The zone and both items are named exports.
| Component | Description | Import from |
|---|---|---|
| FileUpload.Root | A div with flex-col gap-4: 16px between the drop zone and the list. Spreads every div prop. | @/components/application/file-upload/file-upload-base |
| FileUpload.List | A ul with flex-col gap-3, wrapping its children in motion’s AnimatePresence so an item removed from the list lets the rest slide up. Spreads every ul prop. | @/components/application/file-upload/file-upload-base |
| FileUpload.DropZone | The target: a featured icon, a Click to upload button over a hidden file input, or drag and drop, and a hint. Owns drag-over and invalid state, checks accept and maxSize, and hands accepted files to onDropFiles. Also exported as FileUploadDropZone. | @/components/application/file-upload/file-upload-base |
| FileUpload.ListItemProgressBar | One queued file as a motion.li: file icon, name, done of total, a status word, a delete button, and the base ProgressBar with its percentage on the right. Failed swaps the bar for Try again. Also exported as FileListItemProgressBar. | @/components/application/file-upload/file-upload-base |
| FileUpload.ListItemProgressFill | The same file with the progress as a fill sliding across the card behind the text, and the percentage in the supporting row. Also exported as FileListItemProgressFill. | @/components/application/file-upload/file-upload-base |
| getReadableFileSize | bytes → “200 KB”, floored to a whole unit from B up to YB, and “0 KB” for 0. What both items print, and what the new message modal uses for its attachment chips. | @/components/application/file-upload/file-upload-base |
Built from
Seven pieces make up the family; six have their own docs page.
Component | Where it’s used here |
|---|---|
| Button | Click to upload, as color link-color at size md, which opens the picker by clicking the hidden input; and Try again, as link-destructive at size sm, on a failed item. |
| ButtonUtility | The delete button on both items: color tertiary, size xs, Trash01, tooltip Delete, and since 2026-08-23 an aria-label of Delete {name} that wins over the tooltip’s. |
| ProgressBar | The bar item’s track, labelPosition right with min 0 and max 100, named {name} upload through the aria-label prop the base component gained on 2026-08-23 for this family. The fill item draws its own progressbar div instead. |
| FeaturedIcon | The zone’s 40px UploadCloud02 tile: color gray, theme modern, size md, dimmed to opacity 50 when disabled. |
| FileIcon | From @untitledui/file-icons, twice per item: a light and a dark theme, one hidden per color scheme, at 40px, with the item’s type and fileIconVariant. |
| @untitledui/icons | UploadCloud02 in the zone and the uploading status, CheckCircle for complete, XCircle for failed, Trash01 on the delete button. |
The drop zone
The zone as consumers pass it: default, single-image, disabled, rejecting. The sheet’s Hover is under States; all live.
SVG, PNG, JPG or GIF (max. 800x400px)
SVG, PNG, or JPG (max. 800×400px)
SVG, PNG, JPG or GIF (max. 800x400px)
PNG only, up to 10 KB
0 files rejected so far
126px tall, read from the source
py-4 above and below, the 40px featured icon, gap-3, then a 42px text block: the 20px link row, a 4px gap, and the 18px hint. Every one of those is the sheet’s value in the 2026-08-23 parity read, as are the 12px radius, the 1px ring-secondary, the 2px ring-brand on drag-over, and the disabled fill. The zone is a block-level div with no width of its own, so it fills its container; the sheet’s 512 and 343px are its Desktop and Mobile artboards, not a cap.Two copies, one breakpoint
md the button reads “Click to upload and attach files” and the span “or drag and drop” is hidden, since a phone has nothing to drag. The sheet’s Mobile symbol keeps “or drag and drop” at 343px, so this is a recorded divergence the kit keeps on purpose; it also means the button’s accessible name changes with the viewport.The two items
Progress bar and Progress fill, each at In progress, Complete, and Error. Every cell is an li inside the kit’s List.
Tech design requirements.pdf
40 KB of 200 KB
Uploading...
20%
Tech design requirements.pdf
200 KB of 200 KB
Complete
100%
Tech design requirements.pdf
0 KB of 200 KB
Failed
Tech design requirements.pdf
200 KB
40%
Tech design requirements.pdf
200 KB
100%
Tech design requirements.pdf
Upload failed, please try again.
98px and 74px, read from the source
p-4 around a 40px icon and a text block of a 20px name, mt-0.5, and a 20px supporting row, 74px in all. The bar item adds mt-1 and the 20px progress row, 98px. The sheet draws both heights, every padding and gap, the 12px divider, the 16px status icons, and the 28px delete button 8px from the top and end edges, which is -mt-2 -me-2 inside p-4. The fill is bg-secondary and bleeds 1px over the ring since 2026-08-23; at 100 it fades to opacity 0 rather than leaving.The sizes are the kit’s arithmetic
getReadableFileSize floors to a whole unit, so the 204,800 byte fixture reads “200 KB”, 20% of it “40 KB”, and the slideout’s 4,404,019 byte prototype “4 MB”. The bar item prints done of total since 2026-08-23, “40 KB of 200 KB”, with 0 when failed, the sheet’s own line; the fill item prints the total and a percent, which the sheet also draws without a total. Read from the source.A queue
Root holds the zone and a List of three. Delete is the list’s one behavior; removal animates, off under reduced motion.
SVG, PNG, JPG or GIF (max. 800x400px)
Tech design requirements.pdf
200 KB of 200 KB
Complete
100%Dashboard recording.mp4
6 MB of 16 MB
Uploading...
40%Dashboard prototype FINAL.fig
3 MB of 4 MB
Uploading...
80%
SVG, PNG, JPG or GIF (max. 800x400px)
Tech design requirements.pdf
200 KB
100%
Dashboard recording.mp4
16 MB
40%
Dashboard prototype FINAL.fig
4 MB
80%
16px then 12px
Root is flex-col gap-4 and List is flex-col gap-3, the sheet’s 16px between the zone and the list and 12px between items. Every consumer in this repo repeats both strings through className, which merges after the kit’s own and is redundant; two of the three Root consumers also add the padding or margin the prop is for. The list’s AnimatePresence carries initial={false} and the items define no exit, so a deleted item leaves at once and only the survivors animate.States
The zone has five states and one prop, isDisabled; an item has three, all props. Only the buttons have hover treatments.
SVG, PNG, JPG or GIF (max. 800x400px)
Drag a file over this zone
PNG only, up to 10 KB
0 files rejected so far
SVG, PNG, JPG or GIF (max. 800x400px)
Invalid needs a handler
processFiles sets the invalid state inside the branch that calls onSizeLimitExceed or onDropUnacceptedFiles, and that branch runs only when the prop is a function. A zone with accept and no handler discards a wrong file without a word. The two rejecting cells on this page pass both, and count what reached them; the don’t card on the Guidelines tab shows the silent version beside it.An item’s state is two props
failed wins; otherwise progress === 100 is complete and anything else is uploading, a strict equality read from the source, so 99.5 and 100.4 both still read as uploading. The three cells under each item above are the whole state space; the sheet draws the same three as In progress, Complete, and Error. The words belong to the bar item alone: the fill item shows the check or cloud icon beside its percent, and when failed the sentence with no icon.